Skip to content

Adding Interactivity

Adarsh Kumar Maurya edited this page Dec 14, 2018 · 3 revisions

Introducing Stateful Widgets

Hello, and welcome back. This chapter of the tutorial Flutter: Getting Started is all about adding interactivity to your app. The widgets we've seen so far are stateless widgets, meaning that once created they are immutable, as they do not keep any state information. Now if you want to have an app that interacts with your users you expect things to change. For example, if you want to calculate the fuel cost of a trip the result must change based on the user input. If the results couldn't change it would be a very sad app indeed, one that would always return the same price for each and every trip you make. Here's what we'll cover in this chapter.

- State and Stateful Widgets
- Events: OnChanged, OnSubmitted
- TextField & TextEditingController
- DropdownButton & DropDownItems

We'll see what state is in Flutter, and start using stateful widgets. We'll have a look at how to handle events. In our demos we'll use the onChanged and onSubmitted events, but the principles we'll see with those two apply to other events as well. Then we'll see what is arguably the most important user input widget, the TextField, and we'll see how to use the TextEditingController. Another very important widget you need when getting user input is the DropdownButton. Other languages may call it differently, but you certainly already know it. It's a dropdown list where you, as a developer, decide the choices your users have, and those choices are called dropdown items in Flutter.

In order to do all that we'll build an app, step by step.

  • First, it will just have a text field where you'll be able to put your name, and the app will read the name and say hello to the name in the text field.
  • The second app is a fuel consumption calculator. Let's briefly have a look at that. As you can see, we have a form with three text fields; a dropdown that allows you to choose your currency, and two buttons, one to submit your data, and the other one to reset the form.

So once you input the required data; the distance, your car consumption, the fuel price, and your currency you get the total cost of your trip. After you have done that you can reset the form by clicking the Reset button.

We'll also use a little bit of styling, like borders and colors to make the app more appealing to your users. For this app we'll mainly be using the same widgets you already know like column, row, text, raise button, etce. At the end of this chapter you'll know how to use state to interact with users, and this is another building foundation for your Flutter skills.

Let's get started.

Using State in Flutter

So what is state?

State is information that can be read sychronously when the widget
is built and might change during the lifetime of the widget.

State is information that can be used when a widget is built and, that's the important part, can change during the lifetime of a widget. There are two key moments in this definition.

  • First, there is an initialization process when the state is created, and
  • Then there's change during the lifetime of a stateful widget. It's not the widget itself that will change, it's the state.

That's a very important concept in Flutter.

In Flutter classes that inherit "StatefulWidgets" are immutable. It's the state itself that's actually mutable. 

Let's have a look at the main differences between a stateless widget that we've used so far and the stateful widget.

               StateLess vs StateFul Widgets

StateLess Widget
- Does not require a mutable state
- Overrides the build() method
- Use when the UI depends on information in the object itself

StateFul Widget
- Has mutable state
- Overrides the createState() method, and returns a State
- Use when the UI can change dynamically

Well, the most obvious difference is explained by the name itself,the state, StateLess, StateFul, but there is a different implementation as well.

As we've seen before, when a widget extends a stateless widget you need to override the build() method, and in there you create and return the UI that makes sense in your app.

A stateful widget is different. Here you need to override the createState() method that returns a state. So you do not create the UI in the widget, but you return a state that will create the UI for your widget.

The state is also what you'll need to respond to events and changes, and in a way this explains when you need to use one or the other.

When your widgets do not need to change during their lifecycle it's better to extend a stateless widget. When the widget needs to change you'll extend a stateful widget.

In an app both can coexist, so you'll generally use both.

Let's have a look at the steps required when you want to use stateful widgets.

1. Create a Class that Extends a Stateful Widget, that returns a State
2. Create a State class, with properties that may change
3. Implement the Build() method
4. Call the setState() method to make changes 
  • First, you'll create a Class that extends a Stateful Widget, and in the createState method you'll return a State.
  • You'll then create a State class, and in there you'll place the properties that may change during the lifecycle of your widget.
  • Within the State class you create a Build() method that will actually create the UI.

When changes need to happen you'll use the setState() method to actually make the changes you need.

Let's see an example of that before we actually use it in our app.

class HelloInput extends StatefulWidget{
 @override
  State<StatefulWidget> createState()=> _HelloInputState();
}

class _HelloInputState extends State<HelloInput>{
 String name = "";
 @override
 Widget build(BuildContext context){
    return Column(children: <Widget>[
        TextField(
          onChanged: (String string){
            setState((){name = string; });
          }
        ),
        Text("Hello " + name + "!")
    ]);
  }
}

So first we create a Class that extends a StatefulWidget. Here we override the createState() method. Note, the return type is a State. As you can see, the State is a generic element, so you will specify the type of state returned here. The only thing the createState() does here is returning an instance of the _HelloInputState class. So here you can use the fat arrow operator and omit the return statement.

Then you create the State itself. Note that here we are specifying that this state is for the HelloInput widget, and here we create a property called name as an empty string. Next, we override the build() method where we build the UI. We return a column that has a text field inside. When the content of this text field changes this is the onChanged event. We call the setState() method, and in that we change the value of the name property.

Finally, we show the user the updated text that contains Hello plus the updated name property. That's it. We are now ready to start coding our first interactive app with Flutter.

Demo: Hello You!

In this first demo of this chapter we'll make a very simple app. We want to ask the user their name and update the text, so that the user will see hello, plus their name.

From Visual Studio Code let's open the command palette with Ctrl+Shift+P or Ctrl+Command+P(mac) and create a new project called trip_cost_app. We'll use this same project to create the bigger app we'll build during this chapter. That's why this name.

Let's remove the test file, and delete everything except the main method, and then my app class, which is just boilerplate code. Let's also delete the comments, and in the home property let's call a HelloYou object without any parameter. For the title let's type HelloYou.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hello You',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new HelloYou(),
    );
  }
}

Next, let's create the HelloYou widget. This will extend a stateful widget. Here we'll override the createState method, and using the fat arrow operator we'll return a new HelloYouState. From the procedure we have outlined before we have completed the first step.

1. Create a Class that Extends a Stateful Widget, that returns a State
2. Create a State class, with properties that may change
3. Implement the Build() method
4. Call the setState() method to make changes 

Next, we need to create the state. As a naming convention, it's a good idea to put an underscore to emphasize that this class is private. Also, we'll give it the same name as the widget's name, plus state. So, in this case, _HelloYouState. Now let's override the build method. Next, let's create a property that we'll change. It will be a string called name. In the build method we'll return aScaffold with an appBar property whose title is a Text, Hello, and the background color that will use the blue accent color. The body of the scaffold is a container. Let's put some padding for all sides. Let's say 15.0. The child of this container will be a column as we'll place the text field and the text, one below the other. The column contains children that are an array of widgets. The first widget is a TextField.

Now this is the key of this demo. We want to do something when the text changes.

Events
Handle events as properties of Widgets

In other words, we need to handle an event.

Events in Flutter are treated exactly as the other properties. You specify the event you want to handle, in this case, the onChanged event that triggers every time the content of a widget changes.

This will inject the new value of the widget that you can use, for example, to call an outside function, passing the new value.

In our app we'll handle the onChanged() event. In this event the string that we'll call, simply, string will be passed, and inside the function that handles this event we'll call the setState() method. In the setState() method we'll update the name property that we have defined above. Okay, under the TextField let's place a Text widget to give feedback to our user. This will contain a Hello string with a space, plus the name property, and an exclamation point, and with that we have completed the actions required to use the state in our app.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hello Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new HelloYou(),
    );
  }
}

class HelloYou extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HelloYouState();
}

class _HelloYouState extends State<HelloYou> {
  String name = '';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Hello"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                onChanged: (String string) {
                  setState(() {
                    name = string;
                  });
                },
              ),
              Text('Hello ' + name + '!')
            ],
          ),
        ));
  }
}

Okay, let's try this out.

Just type your name in the text field, and even if it's very small I hope you can see that the text under the text field changes as soon as you type something. This is the way the onChanged event works. An alternative to the onChanged property is the onSubmitted property. This will set the state only after the user presses the Enter button on the device keyboard.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hello Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new HelloYou(),
    );
  }
}

class HelloYou extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HelloYouState();
}

class _HelloYouState extends State<HelloYou> {
  String name = '';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Hello"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                decoration: InputDecoration(
                  hintText: 'Please insert your name'
                ),
                onChanged: (String string) {
                  setState(() {
                    name = string;
                  });
                },
              ),
              Text('Hello ' + name + '!')
            ],
          ),
        ));
  }
}

Okay, before we get to the next widget let me show you something that can help your users a lot when you have forms. Let's add to our TextField a decoration that takes an InputDecoration. Among other things, there you can specify a hintText. Let's say the hint will be, please insert your name, and let's see how it looks. You can see that now you are able to guide your users giving hints on what you expect them to type. We'll use the InputDecoration for more changes in the next lessons, but first let's have a look at DropdownButtons and DropDownItems.

Demo: Using a DropDownButton and DropDownItems

A dropdown button lets the user select from a number of items. The button shows the currently selected item, as well as an arrow that opens a menu for selecting another item. Dropdowns in Flutter are built like this.

DropdownButton<String>(
items: <String>['Dollars', 'Euro', 'Pounds'].map((String){
        return DropdownMenuItem<String>(
              value: value,
              child: new Text(value),
              );
           }).toList(),
          onChanged: (_){},
       );

There's a DropdownButton widget that contains a list of items. DropdownButton is a generic type, meaning that it's built as DropdownButton of type T, where the generic type T must represent the type of items in your dropdown.

In the example you see on the screen we are choosing to create a list of strings <String>.

A DropdownButton has an items property that contains one or more DropdownMenuItem widgets.

What you are doing here is creating an array of strings,

DropdownButton<String>(
items: <String>['Dollars', 'Euro', 'Pounds'].map((String){
...

and over that we call the map method. What the map method does is iterate through all the values of the array and perform a function on each of them.

DropdownButton<String>(
items: <String>['Dollars', 'Euro', 'Pounds'].map((String){
        return DropdownMenuItem<String>(
              value: value,
              child: new Text(value),
              );
...

The function returns a DropdownMenuItem that has a value and a child that is a Text widget. Over that we call the toList() method that creates a list containing the elements we have returned.

DropdownButton<String>(
items: <String>['Dollars', 'Euro', 'Pounds'].map((String){
        return DropdownMenuItem<String>(
              value: value,
              child: new Text(value),
              );
           }).toList()

Back to our app. Let's create a DropdownButton that will contain a few currency values. We created between the TextField and the Text as another widget in the column children. So the DropdownButton will be of type String, and in the items property we'll create an array of strings. Here let's place three items. Let's say Dollars, Euro, and Pounds. In case you are wondering why Euro is singular, and the other two are plural, this is because Euro has no plural, or better, the plural of Euro is Euro. Let's call the map method with a value string, and here let's return a DropdownMenuItem widget of type String that will have a value of value, and the child that will be a Text containing value. Let's call the toList method and we're all set.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hello Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new HelloYou(),
    );
  }
}

class HelloYou extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HelloYouState();
}

class _HelloYouState extends State<HelloYou> {
  String name = '';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Hello"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                decoration:
                    InputDecoration(hintText: 'Please insert your name'),
                onChanged: (String string) {
                  setState(() {
                    name = string;
                  });
                },
              ),
              DropdownButton<String>(
                items: ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars']
                    .map((String value) {
                  return DropdownMenuItem<String>(
                      value: value, child: Text(value));
                }).toList(),
                onChanged: (String value) {},
              ),
              Text('Hello ' + name + '!')
            ],
          ),
        ));
  }
}

Let's also handle the onChanged event, as this is required. This will do nothing right now. Let's try these out and see what happens.

Okay, the good news is that when we click on the triangle of the dropdown button we see the three elements we have created.

The bad news is that when we choose an item, well, nothing really happens.This is because we have to actually handle the onChanged event, otherwise, nothing will be updated.

So let's create an array of strings called currencies that will contain the same currencies we have set before, and another property in our state that we call currency that will take a value of Dollars when created. Then let's modify the onChanged property, and inside the function we'll call a method that we'll create shortly, called onDropdownChanged, passing the value. The onDropdownChanged method will require a String as an argument. This will only call the setState method changing the currency property to the new value that's passed. Let's change the items property of our Dropdown Button, so that the map method is called over the currencies array, and its value property takes the currency string.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Hello Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new HelloYou(),
    );
  }
}

class HelloYou extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _HelloYouState();
}

class _HelloYouState extends State<HelloYou> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  String _currency = 'Rupees';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Hello"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                decoration:
                    InputDecoration(hintText: 'Please insert your name'),
                onChanged: (String string) {
                  setState(() {
                    name = string;
                  });
                },
              ),
              DropdownButton<String>(
                items: _currencies.map((String value) {
                  return DropdownMenuItem<String>(
                      value: value, child: Text(value));
                }).toList(),
                value: _currency,
                onChanged: (String value) {
                  _onDropdownChanged(value);
                },
              ),
              Text('Hello ' + name + '!')
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }
}

Let's try these out. As you can see, now out dropdown is working as expected. Okay, we have left the best part for last. Next, we'll transform this HelloYou app with a strange currency dropdown into a fuel cost calculator app.

Demo: Fuel Cost Calculator (Part 1)

In this lesson we'll transform our HelloYou app into the fuel consumption app. As a reminder, the final result will be , a text field for the distance, below that another one for the average consumption, a last text field for the fuel cost, and the dropdown that will allow the user to choose their currency. Under that two buttons, one to calculate the cost of the trip, and another one to reset the form. This will empty all the widgets in the form.

Let's see all that in action. We'll begin by refactoring our app a little bit. First, let's change the title of our MaterialApp from HelloYou to Trip Cost Calculator. Let's also change the name of the HelloYou class into FuelForm. Therefore, we'll need to change the home property of our material app and the state. Now let's also change the state name into FuelFormState, and it's called in the createState() method. I've also commented out the DropdownButton for now, so that we can focus only on the TextField. Now let's change the TextField that's containing the name. In our fuel consumption app this will contain the distance of the trip of our user. This field will only contain numbers. There's a property called keyboardType that takes a value from the TextInputType class. One of those values is number. That's what we need, but there are several other values depending on the content of the field. There's email, phone, URL, and others.

Next, let's think a little bit about the font style we want to use in our app. Flutter has a text theme class that contains definitions for several text styles found in material design, so instead of creating styles directly we can use the styles that are already set there. Let's create a new text style in our build method. We'll call it textStyle, and from our context theme it will use the textTheme.title definition. If you want to have a look at the different TextTheme definitions that are available have a look at the link on the screen.

In the decoration property let's change the hintText into e.g. 124, and let's give it the same style we've given to the text through the labelStyle property. There's also a labelText property. Let's give it a value of Distance. Okay, let's have a look at our app right now, so you can see the difference between the hintText and the labelText. Both serve the same purpose, which is guiding your users in understanding the input you expect, but the hintText stays inside the TextField and disappears after you write something. The labelText is outside of it, as you can see. Also note that the keyboard we see only contains numbers, as we specified a number TextInputType as keyboard. Let's also add a border. We'll use an OutlineInputBorder, and because we want to have it rounded we'll use the borderRadius property, and from the BorderRadius class, which is the circular() method, passing a 5.0 value. Let's see how it looks. Okay, it's as we could expect. Right.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Trip Cost Calculator',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FuelForm(),
    );
  }
}

class FuelForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FuelFormState();
}

class _FuelFormState extends State<FuelForm> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  String _currency = 'Rupees';
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.title;

    return Scaffold(
        appBar: AppBar(
          title: Text("Trip Cost Calculator"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                decoration: InputDecoration(
                  labelText: 'Distance',
                  hintText: "e.g 124",
                  labelStyle: textStyle,
                  border:OutlineInputBorder(
                    borderRadius: BorderRadius.circular(5.0)
                  ),
                ),
                keyboardType: TextInputType.number,
                onChanged: (String string) {
                  setState(() {
                    name = string;
                  });
                },
              ),
              // DropdownButton<String>(
              //   items: _currencies.map((String value) {
              //     return DropdownMenuItem<String>(
              //         value: value, child: Text(value));
              //   }).toList(),
              //   value: _currency,
              //   onChanged: (String value) {
              //     _onDropdownChanged(value);
              //   },
              // ),
              Text('Hello ' + name + '!')
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }
}

Now we need to get the value of the text field. In the HelloYou example we used the onChanged event to write the changes to a text widget. We could use the same approach here. We could create a variable and update it as soon as the content of the text field changes through the onChanged event, but we'll use another approach here. We'll use a TextEditingController. This object is made so that whenever the user modifies a text field with an associated text editing controller the text field updates its value, and the controller can notify any listeners. Let's see how it works. Under the currency string let's create a TextEditingController that we can call distanceController that will be an instance of a TextEditingController.

Now we can set the controller property of our text field. That will be the distance controller. We can now remove the onChanged event, as the controller will automatically deal with text changes.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Trip Cost Calculator',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FuelForm(),
    );
  }
}

class FuelForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FuelFormState();
}

class _FuelFormState extends State<FuelForm> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  String _currency = 'Rupees';
  TextEditingController distanceController = new TextEditingController();
  String result = '';
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.title;

    return Scaffold(
        appBar: AppBar(
          title: Text("Trip Cost Calculator"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                controller: distanceController,
                decoration: InputDecoration(
                  labelText: 'Distance',
                  hintText: "e.g 124",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
              // DropdownButton<String>(
              //   items: _currencies.map((String value) {
              //     return DropdownMenuItem<String>(
              //         value: value, child: Text(value));
              //   }).toList(),
              //   value: _currency,
              //   onChanged: (String value) {
              //     _onDropdownChanged(value);
              //   },
              // ),
              Text('Hello ' + name + '!')
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }
}

In order to see if this is working as expected we'll create a button that will read the value of the distance controller and write its value to the text widget that we used to say hello. Instead of using name as property of our state we'll use result.

Let's uncomment the DropdownButton, and let's also set the text of our form to the result string. Then we'll create a RaisedButton. We'll give it a color of our context primaryColorDark and a textColor of primaryColorLight. As a child it will contain a text with Submit, and the textScaleFactor of 1.5. This will make the text 50% bigger. In the onPressed event we call the setState method. For now we'll only set the result to the content of the distanceController reading distanceController.text.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Trip Cost Calculator',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FuelForm(),
    );
  }
}

class FuelForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FuelFormState();
}

class _FuelFormState extends State<FuelForm> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  String _currency = 'Rupees';
  TextEditingController distanceController = new TextEditingController();
  String result = '';
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.title;

    return Scaffold(
        appBar: AppBar(
          title: Text("Trip Cost Calculator"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                controller: distanceController,
                decoration: InputDecoration(
                  labelText: 'Distance',
                  hintText: "e.g 124",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
              DropdownButton<String>(
                items: _currencies.map((String value) {
                  return DropdownMenuItem<String>(
                      value: value, child: Text(value));
                }).toList(),
                value: _currency,
                onChanged: (String value) {
                  _onDropdownChanged(value);
                },
              ),
              RaisedButton(
                color: Theme.of(context).primaryColorDark,
                textColor: Theme.of(context).primaryColorLight,
                onPressed: () {
                  setState(() {
                    result = distanceController.text;
                  });
                },
                child: Text(
                  'Submit',
                  textScaleFactor: 1.5,
                ),
              ),
              Text(result)
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }
}

If we try these out we can see that the result is updated, and the text widget contains the result, so the controller is updated as soon as the text field content changes, which is quite handy, as we do not have to call the onChanged method each time we need to update the values. Now we are close. In the next lesson we'll complete the fuel cost calculator app.

Demo: Fuel Cost Calculator (Part 2)

Let's copy the TextField and paste it twice. We want to create a TextField for the fuel consumption and one for the fuel price. In the first TextField we'll change the labelText to Distance per Unit. In Italy, where I live, we would say Kilometers per Liter, but depending on where you live you might also say Miles per Gallon. As we want to build an app that works everywhere, let's write a labelText with Distance per Unit. Probably no one would really understand what this means anywhere in the world, but we're creating an international app, aren't we. Okay, let's also change the hintText to e.g. 17. These are kilometers. I guess miles per gallon would be a higher number, like 40 or 50. Let's do the same with the third text field. This will contain the price for the fuel in your country. We'll change both the hintText and the labelText to e.g. 1.65 and price. Now we need two more controllers. Let's call the second one avgController for the average consumption, and the last one priceController. We can also change the controller property of the remaining two text fields. It will be avgController for the second TextField, and priceController for the last one. Let's have a look at our app so far. We now have three text fields, and they are a bit too close together, but we will fix that shortly, and we want to give our user a result, not just the distance like now.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Trip Cost Calculator',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FuelForm(),
    );
  }
}

class FuelForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FuelFormState();
}

class _FuelFormState extends State<FuelForm> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  String _currency = 'Rupees';
  TextEditingController distanceController = new TextEditingController();
  TextEditingController avgController = new TextEditingController();
  TextEditingController priceController = new TextEditingController();
  String result = '';
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.title;

    return Scaffold(
        appBar: AppBar(
          title: Text("Trip Cost Calculator"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                controller: distanceController,
                decoration: InputDecoration(
                  labelText: 'Distance',
                  hintText: "e.g 124",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
                  TextField(
                controller: avgController,
                decoration: InputDecoration(
                  labelText: 'Distance per Unit',
                  hintText: "e.g 17",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
                  TextField(
                controller: priceController,
                decoration: InputDecoration(
                  labelText: 'Price',
                  hintText: "e.g 75",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
              DropdownButton<String>(
                items: _currencies.map((String value) {
                  return DropdownMenuItem<String>(
                      value: value, child: Text(value));
                }).toList(),
                value: _currency,
                onChanged: (String value) {
                  _onDropdownChanged(value);
                },
              ),
              RaisedButton(
                color: Theme.of(context).primaryColorDark,
                textColor: Theme.of(context).primaryColorLight,
                onPressed: () {
                  setState(() {
                    result = distanceController.text;
                  });
                },
                child: Text(
                  'Submit',
                  textScaleFactor: 1.5,
                ),
              ),
              Text(result)
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }
}

So back to our code, we'll create a method called calculate that will return a string. Inside this method let's create a double called distance that will parse the distanceController.text, and let's do the same for the fuelCost and the consumption that will read the avgController.text. Now let's calculate the total cost. That will be a double as well, and will be the distance divided consumption times fuelCost. The result will be a string with The total cost of your trip is plus totalCost.toStringAsFixed of two, so that the user will only see two decimals in the result. We'll also concatenate a space and the selected currency, and finally, return the result. Now in the setState method we'll call the calculate function. Let's try this out. We put a distance of 200, an average consumption of 55, and the price of $2.9. If those numbers do not make sense where you live just change them. We can see that this trip will cost $10.55. I hope this amount will get you to a beautiful seaside or mountain location.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Trip Cost Calculator',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FuelForm(),
    );
  }
}

class FuelForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FuelFormState();
}

class _FuelFormState extends State<FuelForm> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  String _currency = 'Rupees';
  TextEditingController distanceController = new TextEditingController();
  TextEditingController avgController = new TextEditingController();
  TextEditingController priceController = new TextEditingController();
  String result = '';
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.title;

    return Scaffold(
        appBar: AppBar(
          title: Text("Trip Cost Calculator"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              TextField(
                controller: distanceController,
                decoration: InputDecoration(
                  labelText: 'Distance',
                  hintText: "e.g 124",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
              TextField(
                controller: avgController,
                decoration: InputDecoration(
                  labelText: 'Distance per Unit',
                  hintText: "e.g 17",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
              TextField(
                controller: priceController,
                decoration: InputDecoration(
                  labelText: 'Price',
                  hintText: "e.g 75",
                  labelStyle: textStyle,
                  border: OutlineInputBorder(
                      borderRadius: BorderRadius.circular(5.0)),
                ),
                keyboardType: TextInputType.number,
              ),
              DropdownButton<String>(
                items: _currencies.map((String value) {
                  return DropdownMenuItem<String>(
                      value: value, child: Text(value));
                }).toList(),
                value: _currency,
                onChanged: (String value) {
                  _onDropdownChanged(value);
                },
              ),
              RaisedButton(
                color: Theme.of(context).primaryColorDark,
                textColor: Theme.of(context).primaryColorLight,
                onPressed: () {
                  setState(() {
                    result = _calculate();
                  });
                },
                child: Text(
                  'Submit',
                  textScaleFactor: 1.5,
                ),
              ),
              Text(result)
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }

  String _calculate() {
    double _distance = double.parse(distanceController.text);
    double _fuelCost = double.parse(priceController.text);
    double _consumption = double.parse(avgController.text);
    double _totalCost = _distance / _consumption * _fuelCost;
    String _result = 'The total cost of your trip is ' +
        _totalCost.toStringAsFixed(2) +
        ' ' +
        _currency;
    return _result;
  }
}

Not bad, but in order to complete our app let's also make the UI a bitmore user friendly and add the Reset button. Let's put some distance between the text fields. We'll create a constant for that. Let's call it formDistance, and let's give it a value of 5.0. Then let's enclose the TextFields into a Padding widget. We'll set a padding property of EdgeInsets.only for the top and bottom, both will have the value of the formDistance.

Let's repeat the same process for all the three TextFields. So we'll add another padding for the Distance per Unit TextField with the same top and bottom values, and we'll repeat the process for the price. Now for the price we want the currency button list to be in the same row as the price. So let's create a row that as children will contain both the TextField and the button. Let's also include both of them in an expanded widget, so that they will fill the available space.

Let's repeat the process for the DropdownButton widget. We'll also put a container between them that would be five times the formDistance constant. Let's add a second button for the reset of our four. Let's copy the existing raised button, and paste it just below. Let's change the text to Reset.

On the onPressed event we'll call a reset function that will clear the text fields and the result text. This will be in the same line as the submit button, so let's create another row. We then close the button in an expanded widget. We will do the same for the Reset button, so let's include it in an expanded widget as well.

Let's also change the reset button colors, so that the reset button will have buttonColor as color, and the primaryColorDark as text color. We need to write the reset function, which will return void and will call reset. This will change the text of the distance controller to an empty string, and it will do the same with the avgController.text and the priceController.text, and we'll call the setState method, setting the result to an empty string as well.Okay, this should complete our app.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Trip Cost Calculator',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FuelForm(),
    );
  }
}

class FuelForm extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _FuelFormState();
}

class _FuelFormState extends State<FuelForm> {
  String name = '';
  final _currencies = ['Rupees', 'Euro', 'Pounds', 'Yens', 'Dollars'];
  final double _formDistance = 5.0;
  String _currency = 'Rupees';
  TextEditingController distanceController = new TextEditingController();
  TextEditingController avgController = new TextEditingController();
  TextEditingController priceController = new TextEditingController();
  String result = '';
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.title;

    return Scaffold(
        appBar: AppBar(
          title: Text("Trip Cost Calculator"),
          backgroundColor: Colors.blueAccent,
        ),
        body: Container(
          padding: EdgeInsets.all(15.0),
          child: Column(
            children: <Widget>[
              Padding(
                  padding: EdgeInsets.only(
                      top: _formDistance, bottom: _formDistance),
                  child: TextField(
                    controller: distanceController,
                    decoration: InputDecoration(
                      labelText: 'Distance',
                      hintText: "e.g 124",
                      labelStyle: textStyle,
                      border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(5.0)),
                    ),
                    keyboardType: TextInputType.number,
                  )),
              Padding(
                  padding: EdgeInsets.only(
                      top: _formDistance, bottom: _formDistance),
                  child: TextField(
                    controller: avgController,
                    decoration: InputDecoration(
                      labelText: 'Distance per Unit',
                      hintText: "e.g 17",
                      labelStyle: textStyle,
                      border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(5.0)),
                    ),
                    keyboardType: TextInputType.number,
                  )),
              Padding(
                  padding: EdgeInsets.only(
                      top: _formDistance, bottom: _formDistance),
                  child: Row(children: <Widget>[
                    Expanded(
                        child: TextField(
                      controller: priceController,
                      decoration: InputDecoration(
                        labelText: 'Price',
                        hintText: "e.g 75",
                        labelStyle: textStyle,
                        border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(5.0)),
                      ),
                      keyboardType: TextInputType.number,
                    )),
                    Container(width: _formDistance * 5),
                    Expanded(
                        child: DropdownButton<String>(
                      items: _currencies.map((String value) {
                        return DropdownMenuItem<String>(
                            value: value, child: Text(value));
                      }).toList(),
                      value: _currency,
                      onChanged: (String value) {
                        _onDropdownChanged(value);
                      },
                    ))
                  ])),
              Row(children: <Widget>[
                Expanded(
                    child: RaisedButton(
                  color: Theme.of(context).primaryColorDark,
                  textColor: Theme.of(context).primaryColorLight,
                  onPressed: () {
                    setState(() {
                      result = _calculate();
                    });
                  },
                  child: Text(
                    'Submit',
                    textScaleFactor: 1.5,
                  ),
                )),
                Expanded(
                    child: RaisedButton(
                  color: Theme.of(context).buttonColor,
                  textColor: Theme.of(context).primaryColorDark,
                  onPressed: () {
                    setState(() {
                      reset();
                    });
                  },
                  child: Text(
                    'Reset',
                    textScaleFactor: 1.5,
                  ),
                ))
              ]),
              Text(result)
            ],
          ),
        ));
  }

  _onDropdownChanged(String value) {
    setState(() {
      this._currency = value;
    });
  }

  String _calculate() {
    double _distance = double.parse(distanceController.text);
    double _fuelCost = double.parse(priceController.text);
    double _consumption = double.parse(avgController.text);
    double _totalCost = _distance / _consumption * _fuelCost;
    String _result = 'The total cost of your trip is ' +
        _totalCost.toStringAsFixed(2) +
        ' ' +
        _currency;
    return _result;
  }

  void _reset() {
    distanceController.text = '';
    avgController.text = '';
    priceController.text = '';
  }
}

Let's have a look at the result.

We can try to use our app again, and we see that it's working perfectly. Let's also try the Reset button, and this cleans up everything.Well done. You now have an international fuel cost calculator app, but more important, you now know how to deal with state in Flutter.

Now the next step would be to have this app more modularized, but we'll see how to do this in the next chapter.

Let's briefly recap what we've done in this chapter.

  • The core of this chapter has been seeing how to use stateful widgets in Flutter.
  • We've seen how to use the state class, we've seen when to call the setState() method to make changes to a state. -For our UI widgets we've seen the DropdownButton and DropdownButtonItems.
  • We've seen the user of the map() method and the onChanged() event. - --- We've used several TextFields with their controllers and style properties. - We've also seen how to choose the keyboardType and the decoration property. - We've used the padding widget to deal with spacing for our widgets, and we've also used border, borderRadius, and textScaleFactor.

Wow. This was quite a lot of work, but the best part comes next when we'll deal with data within a Flutter app. See you there.

Clone this wiki locally