Home Tutorials Training Consulting Books Company Contact us


Get more...

This tutorial follows the General Flutter tutorial and gives a introduction into developing a flutter application with hands-on projects.

1. Exercise: Build a simple, interactive Flutter app

This exercise assumes that you have a newly created Flutter app, called flutterui. This is also the folder in which this app is located.

See Creating a new Flutter project with VSCode for creating this application.

This should looks similar to the following:

Starting point

In this exercise you modify the generated flutterui Flutter app.

You will learn:

  • How to import a Dart file into another Dart file

  • How to implement a button and how to show a dialog

1.1. Remove generated test class

The template has generated a test class, which is good but for the purpose of this exercise not helpful, as it will give us errors if we change the application without adjusting the test. To continue learning basic app development, delete the test\widget_test.dart file.

Of course, software testing is important but it is also good to focus on learning basic application development.

1.2. Change generated application

As a first step, lets change the generated application. Open the lib/main.dart file, delete the whole content and replace it with the following code.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

Run this application and ensure that the text is displayed on the emulator.

1.3. Hide unwanted warnings

The Flutter tooling checks during development for bad programming patterns. Certain checks depend on the code and as we modify during development this code constantly, certain warnings makes is harder to modify the code.

Therefore, while you are learning Flutter and constantly changing your code, you may want to turn off a few of these warnings. During the final touches of app development (before publishing it), you would enable these warnings again and fix them.

To ignore a few warnings which show a lot of warnings during development, you can open the analysis_options.yaml file and replace its content with the following:

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_const_constructors_in_immutables: false
    prefer_const_literals_to_create_immutables: false

1.4. Splitting up the code in multiple dart files

Splitting up your code in multiple files, helps keeping an overview of your code. Therefore, in the lib folder, create a new file named homepage.dart with the following content.

import 'package:flutter/material.dart';

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage(this.title);      (1)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Replace this with an ElevatedButton'),
          ],
        ),
      ),
    );
  }
}
1 Constructor parameter so that you can set the title in the application bar.

Change the code in the main.dart file to use the new MyHomePage widget.

First you need to import the homepage.dart file, this gives you access to the widgets in this file.

Afterwards change the code to the following

import 'package:flutter/material.dart';
import 'package:flutterui/homepage.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: MyHomePage("Testing"),    (1)
    );
  }
}
1 This uses your new widget MyHomePage as property of the home key for the material app.

Start you app. You may have to restart your application, hot reload does not always work if you make bigger changes to your application.

Run your application afterwards and ensure you see the Replace this with an ElevatedButton.

homepage widget10

1.5. Use an ElevatedButton

Replace the Text widget in your MyHomePage widget with an ElevatedButton.

ElevatedButton(
    onPressed: () {
        print("hello");
    },
    child: Text('Show dialog'),      (1)
),
1 trailing comma for consistent formatting. It is recommended to always use a trailing comma after setting attributes or widgets. This leads to a consistent and readable formating.

The full code in main.dart should look like the following:

import 'package:flutter/material.dart';

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage(this.title); (1)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {
                print("hello");
              },
              child: Text('Show dialog'),
            ), (2)
          ],
        ),
      ),
    );
  }
}

1.6. Run the application

Do a hot reload and ensure you see now the button on the emulator. If you are using an IDE you simply need to save, this triggers a hot reload.

homepage widget20

If you press the button, you should see the output in your DEBUG CONSOLE in VsCode.

1.7. Wrap your body widget into a new widget

Open your homepage.dart`file and check the `body property.

The body property of the MyHomePage widget defines a Center widget as value. This child widget contains several other widgets.

Extract this widgets so that the code is easier to read. Therefore, move the widget assigned to the body property into a custom widget called MyContent.

Put this widget in the homepage.dart file, Dart does not require that every widget is in its own file.

Show Solution
class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ElevatedButton(
            onPressed: () {
              print("hello");
            },
            child: Text('Show dialog'),
          ),
        ],
      ),
    );
  }

Use your new widget in your MyHomepage widget.

Show Solution
class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage(this.title);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: MyContent(),
    );
  }
}

1.8. Show a message (snackbar) once the button is pressed

Now you want to display a message once the ElevatedButton is clicked. According to the material design specification a snackbar is a brief messages about app processes at the bottom of the screen. Have a short look at https://material.io/components/snackbars/ to get a visual impression about the usage.

Use the following private method in your MyContent widget. This method allows to show a snackbar.

SnackBar _createSnackBar() {
  var snackbar = SnackBar(
    content: Text('This is your message'),
    action: SnackBarAction(
      label: 'Delete',
      onPressed: () {},
    ),
  );
  return snackbar;
}

Show this snackbar if the ElevatedButton is pressed.

ElevatedButton(
  onPressed: () {
    var snackebar = _createSnackBar();
    ScaffoldMessenger.of(context).showSnackBar(snackebar);
  },
  child: Text('Show dialog'),
),
Show Solution
class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ElevatedButton(
              onPressed: () {
                var snackbar = _createSnackBar();
                ScaffoldMessenger.of(context).showSnackBar(snackbar);
              },
              child: Text('Show dialog'),
          ),
        ],
      ),
    );
  }

  SnackBar _createSnackBar() {
    var snackbar = SnackBar(
      content: Text('This is your message'),
      action: SnackBarAction(
        label: 'Delete',
        onPressed: () {},
      ),
    );
    return snackbar;
  }
}
snackbar

The created Snackbar is not floating so it doesn’t looks like the material design specification. Set the behavior: SnackBarBehavior.floating property on your snackbar to get the material design look and feel.

 SnackBar _createSnackBar() {
    var snackbar = SnackBar(
      behavior: SnackBarBehavior.floating,
      content: Text('This is your message'),
      action: SnackBarAction(
        label: 'Delete',
        onPressed: () {},
      ),
    );
    return snackbar;
  }
snackbar floating

1.9. Showing an alert after the button click

You now want to show a custom dialog (alert) to your users.

Create the following private function in the homepage.dart file to your MyContent widget.

Future<void> _ackAlert(BuildContext context) {
  return showDialog<void>(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text('Not in stock'),
        content: const Text('This item is no longer available'),
        actions: <Widget>[
          TextButton(
            child: Text('OK'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      );
    },
  );
}

Add a TextButton before the ElevatedButton as child to the Column of your MyContent widget.

Call the _ackAlert function, if the user presses the TextButton.

Show Solution
import 'package:flutter/material.dart';

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage(this.title); (1)

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: MyContent(),
    );
  }
}

class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextButton(
            onPressed: () {
              _ackAlert(context);
            },
            child: Text('Show alert'),
          ),
          ElevatedButton(
            onPressed: () {
              var snackbar = _createSnackBar();
              ScaffoldMessenger.of(context).showSnackBar(snackbar);
            },
            child: Text('Show dialog'),
          ),
        ],
      ),
    );
  }

  SnackBar _createSnackBar() {
    var snackbar = SnackBar(
      content: Text('This is your message'),
      action: SnackBarAction(
        label: 'Delete',
        onPressed: () {},
      ),
    );
    return snackbar;
  }

  Future<void> _ackAlert(BuildContext context) {
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Not in stock'),
          content: const Text('This item is no longer available'),
          actions: <Widget>[
            TextButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}
no stock dialog10
no stock dialog20

1.10. Cleanup your implementation

The MyContent widget has grown in size. Move it to its own file called content.dart and import this file in your homepage.dart file to use it there.

content.dart should look like this:

import 'package:flutter/material.dart';

class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextButton(
            onPressed: () {
              _ackAlert(context);
            },
            child: Text('Show alert'),
          ),
          ElevatedButton(
            onPressed: () {
              var snackebar = _createSnackBar();
              ScaffoldMessenger.of(context).showSnackBar(snackebar);
            },
            child: Text('Show dialog'),
          ),
        ],
      ),
    );
  }

  SnackBar _createSnackBar() {
    var snackbar = SnackBar(
      behavior: SnackBarBehavior.floating,
      content: Text('This is your message'),
      action: SnackBarAction(
        label: 'Delete',
        onPressed: () {},
      ),
    );
    return snackbar;
  }

  Future<void> _ackAlert(BuildContext context) {
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Not in stock'),
          content: const Text('This item is no longer available'),
          actions: <Widget>[
            TextButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

homepage.dart should look like this:

import 'package:flutter/material.dart';
import 'package:flutterui/content.dart';

class MyHomePage extends StatelessWidget {
  final String title;
  MyHomePage(this.title);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: MyContent(),
    );
  }
}

1.11. Wrap your buttons

What happens, if you add 20 more buttons to your MyContent widget?

Now you can use a for loop to generate widgets as additional child.

  for (int i = 1; i < 20; i++)
    ElevatedButton(
      onPressed: () {},
      child: Text(i.toString()),
  ),
Show Solution
class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextButton(
              onPressed: () {
                _ackAlert(context);
              },
              child: Text('Show alert')),
          ElevatedButton(
              onPressed: () {
                var snackebar = _createSnackBar();
                ScaffoldMessenger.of(context).showSnackBar(snackebar);
              },
              child: Text('Show dialog'),
             ),
          for (int i = 1; i < 20; i++)
            ElevatedButton(
              onPressed: () {},
              child: Text(i.toString()),
            ),
        ],
      ),
    );
  }
    // the methods for snakebar and dialog
}
buttons overflow flutter10

Column does not allow to scroll nor does it wrap. Flutter provides other widgets for this purpose. For example, use the Wrap widget, instead of the Column widget as container. In a Wrap container the attribute 'mainAxisAlignment: MainAxisAlignment.center' can not be used, so remove it.

class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Wrap(
        //mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextButton(
              onPressed: () {
                _ackAlert(context);
              },
              child: Text('Show alert')),
          ElevatedButton(
              onPressed: () {
                var snackebar = _createSnackBar();
                ScaffoldMessenger.of(context).showSnackBar(snackebar);
              },
              child: Text('Show dialog'),
            ),
          for (int i = 1; i < 20; i++)
            ElevatedButton(
              onPressed: () {},
              child: Text(i.toString(),
             ),
            ),
        ],
      ),
    );
  }
  // the methods for snakebar and dialog
}
buttons overflow flutter20

Now increase the number of buttons to 200, even with wrap not all buttons can be displayed.

Put the Wrap widget into a SingleChildScrollView to make it scrollable.

Show Solution
import 'package:flutter/material.dart';

class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: SingleChildScrollView(
        child: Wrap(
          children: <Widget>[
            TextButton(
                onPressed: () {
                  _ackAlert(context);
                },
                child: Text('Show alert')),
            ElevatedButton(
                onPressed: () {
                  var snackebar = _createSnackBar();
                  ScaffoldMessenger.of(context).showSnackBar(snackebar);
                },
                child: Text('Show dialog'),
             ),
            for (int i = 1; i < 200; i++)
              ElevatedButton(
                onPressed: () {},
                child: Text(i.toString()),
              ),
          ],
        ),
      ),
    );
  }

  SnackBar _createSnackBar() {
    var snackbar = SnackBar(
      behavior: SnackBarBehavior.floating,
      content: Text('This is your message'),
      action: SnackBarAction(
        label: 'Delete',
        onPressed: () {},
      ),
    );
    return snackbar;
  }

  Future<void> _ackAlert(BuildContext context) {
    return showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text('Not in stock'),
          content: const Text('This item is no longer available'),
          actions: <Widget>[
            TextButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }
}

2. Exercise: Create the basic structure for an Github issue application

In this exercise you create the build blocks for an extended application which you will extend in other exercises. It will become a full featured Github client to view issues.

2.1. Create the App

Create a new Flutter application github_viewer.

2.2. Create the Homepage

The Widget defined via the home in your material app is the page the user sees when they start the app. Generally it should contain all necessary navigation elements to let the user navigate to other screens.

Create a new file page/homepage.dart in the lib directory. (Also create the page directory if it does not exist yet.)

Create the following class inside this new file.

import 'package:flutter/material.dart';

class Homepage extends StatefulWidget {
  @override
  _HomepageState createState() => _HomepageState();
}

class _HomepageState extends State<Homepage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GitHub Issue Viewer'),
        actions: <Widget>[
          IconButton(
            onPressed: () {
              print('AppBar action pressed'); (1)
            },
            icon: Icon(Icons.settings),
          )
        ],
      ),
      body: Text('Welcome to the homepage'), (1)
    );
  }
}
1 These are placeholders which will be replaced in a later exercise

2.3. Adjust the main.dart File

Navigate back to the main.dart file and change it to the following.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'page/homepage.dart';

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

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Homepage(),
    );
  }
}

2.4. Start the Application

Start the app (if it’s not started already). It should display the text Welcome to the homepage and have a blue app bar on top displaying GitHub Issue Viewer. On the right there should be a Settings icon and if pressed it should display the text AppBar action pressed in the console.

github issues first incarnation

3. Exercise: Building your own Widget

In this exercise you are going to extend the GitHub Issue Viewer application.

By now you should have the following file structure:

lib/
  page/
    homepage.dart
  main.dart

You will now build a reusable Widget that represents an entry in the overview of issues in a repository.

To make the development easier create the data model for the issue data. This is a basic data class that will later be used to fill it with data returned by the GitHub API.

Open the page/homepage.dart file and add the following at the bottom of the file:

// ...
class Issue { (1)
  int number = 1;
  String state = 'open';
  String title = 'Issue title';
  String body = 'Issue body';
  String userName = 'userName';
  DateTime createdAt = DateTime.now();
}

class _IssueCard extends StatelessWidget {
  final Issue issue;

  _IssueCard(this.issue);

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        child: ListTile(
          title: Text('${issue.title} #${issue.number}'),
          subtitle: Text(
              '${issue.userName} opened on ${issue.createdAt}'),
          trailing: Icon(Icons.arrow_forward),
          onTap: () {
            print('Issue tile tapped');
          },
        ),
      ),
    );
  }
}
1 The data model is currently statically initialized with default data. That will change later.

To use the class go back to the _HomepageState class definition. Replace the body: Text(…​) line with the following content:

      body: ListView.builder((1)
        itemCount: 4,
        itemBuilder: (context, index) {
          return _IssueCard(Issue());
        },
      ),
1 A ListView.builder is a special constructor in the ListView class that calls the method that is passed to its itemBuilder parameter for the number of times passed to its itemCount parameter. In this case it will call the method 4 times. The current index is the second parameter of the method.

The _HomepageState class should now look like this:

class _HomepageState extends State<Homepage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GitHub Issue Viewer'),
        actions: <Widget>[
          IconButton(
            onPressed: () {
              print('AppBar action pressed');
            },
            icon: Icon(Icons.settings),
          )
        ],
      ),
      body: ListView.builder(
        itemCount: 4,
        itemBuilder: (context, index) {
          return _IssueCard(Issue());
        },
      ),
    );
  }
}

The resulting app should look like this:

Resulting App

4. Exercise: Use the FutureBuilder

In this exercise the FutureBuilder is used to add issues from a GitHub repository to the HomePage widget.

Your app’s folder structure should look like this:

lib/
  page/
      homepage.dart
    main.dart

Add the http library dependency to the pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter

# ... potentially more dependencies
  http: 0.12.0+2 (1)
# ... potentially more dependencies
1 Use the same indent as the flutter entry

4.1. Building the Skeleton

As GitHub’s API is a little bit restrictive with usage limitations it’s best to start by using some dummy data, before making the actual requests to the API. You might run out of quota pretty soon.

Open the page/homepage.dart file. Go to the _HomepageState class and add a basic function that returns a List of Issue:

class _HomepageState extends State<Homepage> {

    Future<List<Issue>> getIssues() async {
        return [Issue(), Issue(), Issue()]; (1)
    }

// ...more
1 The Issue class is the data class from before. For now just use three of it’s instances with the default "dummy" data.

4.2. FutureBuilder

The next step is to add the FutureBuilder to the app. The body of the Scaffold created in the _HomepageState class will be dynamically created.

Therefore, remove the current value of the body attribute of the Scaffold of the _HomepageState and replace it with the following snippet:

FutureBuilder(
    future: getIssues(),
    builder: (BuildContext context, AsyncSnapshot<List<Issue>> snapshot) {
        if (snapshot.hasData) {
            return ListView.builder(
                itemCount: snapshot.data.length, (1)
                itemBuilder: (BuildContext context, int index) {
                    return _IssueCard(snapshot.data[index]); (2)
                },
            );
        } else if (snapshot.hasError) {
            return Text(snapshot.error.toString());
        } else {
            return CircularProgressIndicator();
        }
    },
),
1 We use the same ListView here as before, but now its itemCount is set to the length of issues returned by the API
2 As we use the builder we have access to the index of the current iteration and can use it to access the issue at the current index. We can be sure that no RangeError (e.g. index out of bounds) will occur because we set the itemCount of the ListView.builder.
One thing to keep in mind with this snippet is that, it calls getIssues() every time it is built. This will lead to many calls to the API and might lock you out of the GitHub API in the next step. A solution to this is the AsyncMemoizer which stores a value and keeps it in state. This means that the value is kept even through hot-reloads.

4.3. Fetching the Data

GitHub’s API is pretty straightforward to use but a little bit restrictive on usage limitations.

The next step is to add the actual fetching of data from the API. For this it is required to import the following packages at the top of the homepage.dart file:

import 'package:http/http.dart' as http;
import 'dart:convert';

Next, adapt the getIssues() function:

Future<List<Issue>> getIssues() async {
    const url = 'https://api.github.com/repos/flutter/flutter/issues';
    var response = await http.get(url);
    var data = jsonDecode(response.body);
    if (response.statusCode != 200) {
        print('The API fetch failed: $data');
        return Future.error(data);
    }

    return data
            .map<Issue>((entry) => Issue()
                ..number = entry['number']
                ..state = entry['state']
                ..title = entry['title']
                ..body = entry['body']
                ..userName = entry['user']['login']
                ..createdAt = DateTime.parse(entry['created_at']))
            .toList();
}

After the next reload the homepage should look like this:

With GitHub API data

4.4. Optional: Switch to Stackoverflow API

Github API currently only allows 60 calls per IP, so you may run during testing into this limitation.

It is pretty easy to switch with your client to another API, lets say Stackoverflow.

5. Exercise: Calling native Android code

Execute the exercise from Google for integrating native code. For this exercise it is easier to do this step in Android Studio, as you are modifying also Java code and Java code assists is helpful for this step.

6. Optional exercise: Add IssueDetail page

In the next step, add a detail page to display the details about an issue. These are the requirements:

  • The page should open after a click on one of the ListTiles on the home page

  • The detail page should receive the clicked issue as a constructor parameter

  • It should display the issue’s body and its comments

  • Optional: The issues on GitHub support markdown formatting. Use the markdown package to render the body of the issue.

The full page should look similar to this:

Final issue detail page

6.1. Resources

Use the Navigator class to open a new route in the onTap function of the IssueCard.

Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => IssuePage(issue)),
    );

Use the FutureBuilder to render the fetched comments only after you got them.

7. Activate code checks

It is time to inherit to Dart conventions by activating automatic checks for it via the Dart linter.

Create the analysis_options.yaml file in your top-level folder. Add the following content.

linter:
  rules:
    - avoid_empty_else
    - avoid_init_to_null
    - avoid_relative_lib_imports
    - avoid_return_types_on_setters
    - avoid_shadowing_type_parameters
    - avoid_types_as_parameter_names
    - curly_braces_in_flow_control_structures
    - empty_catches
    - empty_constructor_bodies
    - library_names
    - library_prefixes
    - no_duplicate_case_values
    - null_closures
    - prefer_contains
    - prefer_equal_for_default_values
    - prefer_is_empty
    - prefer_is_not_empty
    - prefer_iterable_whereType
    - recursive_getters
    - slash_for_doc_comments
    - type_init_formals
    - unawaited_futures
    - unnecessary_const
    - unnecessary_new
    - unnecessary_null_in_if_null_operators
    - unrelated_type_equality_checks
    - use_rethrow_when_possible
    - valid_regexps

The Flutter and Dart compiler should start complaining about violations against this standard. Fix them until you have no more errors. If you see no errors / warnings, try adding the new keyword in front of your widgets and ensure that you see a warning.

8. Exercise: Build a Web Monitor to track the status

In this exercise, you are going to build one app to monitor the status of one input URL.

8.1. Setup

To properly setup the project environment, you need to add 'Network' module to our dependency. To add 'Network', simply put following code in 'pubspec.yaml'.

dependencies:
  flutter:
    sdk: flutter
  http: ^0.12.1

8.2. My Main Page

In the Main section, you used a widget called MyMainPage as the widget for the main page layout.

First develop the MyMainPage extended from StatefulWidget, since you will interact with users to get the input.

import 'package:flutter/material.dart';

import 'input_page.dart';
import 'section.dart';
import 'data.dart';

class MyMainPage extends StatefulWidget {
  @override
  _MyMainPageSate createState() => new _MyMainPageSate();
}

class _MyMainPageSate extends State<MyMainPage> {
  var urls = new List<Data>();

 ...

'Scaffold' is used as the main container for the whole structure. Then you use AppBar to add our label for the page, also one + sign for the button for make input.

AppBar is used here to allow us to interact with its button. It is 'IconButton' you used below. Widget with similar function is also possible. You can simply add one 'Button' widget in the corner. For simplicity, you use AppBar here.

By clicking the AppBar, app will be directed to the second page, where you can type in our input.

In the body section, you will create a list of Sections added by user. This will use the 'ListView' function, which is explained in previous tutorials.

 return new Scaffold(
        appBar: AppBar(
          title: const Text('Web Monitor'),
          backgroundColor: Colors.blueGrey[900],
          actions: <Widget>[
            IconButton(
                icon: Icon(Icons.add),
                onPressed: () {
                  //TODO create a input page to parse input
                },
          )
          ]
        ),
        body: Center(
          child: new ListView(
            children: sections,
            scrollDirection: Axis.vertical,
          ),
        ),
      );

And since you need to get the input from the second page. You need to use the future function in Flutter.

So you push one context into the second page. And the context will pop up later with the input data you get from the second page.

inputPage(BuildContext context, Widget page) async {
    final dataFromInputPage = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => page),
    ) as Data;

    var data = new Data(seconds: dataFromInputPage.seconds, text: dataFromInputPage.text);

    if(data.text!="null"){
      //TODO add new section for the corresponding data you have
    }
  }

And now you goes into the second page to see how to handle the input. Then you get back here to see how you make use of the input you get.

8.3. Main

Change the main.dart to the following:

import 'package:flutter/material.dart';

import 'my_main_page.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyMainPage(),
    );
  }
}

8.4. Input Page

In this section, you mainly focus on how to get input from user and pass back to previous class.

First, in one stateful widget, you have our main layout designed as follows. Two textfield with text controller to get the input, one for url. And the other one for the time interval at which the app is going to make request.

new Scaffold(
        appBar: AppBar(
          title: const Text('Type In'),
          backgroundColor: Colors.blueGrey[900],
        ),
        body: Center(
          child: Container(
            height: 300,
              child: Column(
              children: <Widget>[
                    TextField(
                          autofocus: true,
                          decoration: new InputDecoration(
                            focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.greenAccent, width: 5.0),
                          ),
                          enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.red, width: 5.0),
                          ),
                          labelText: 'Enter Valid URL', hintText: 'https://www.vogella.com'),
                          controller: myController,
                    ),
                    TextField(
                          autofocus: true,
                          decoration: new InputDecoration(
                            focusedBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.greenAccent, width: 5.0),
                          ),
                          enabledBorder: OutlineInputBorder(
                            borderSide: BorderSide(color: Colors.red, width: 5.0),
                          ),
                          labelText: 'Enter the Time Interval', hintText: 'e.g. 10'),
                          controller: myTimeController,
                    ),
                new FlatButton(
                  child: const Text('CANCEL'),
                  onPressed: () {
                    Navigator.pop(context, d);
                  }),
                new FlatButton(
                  child: const Text('OK'),
                  onPressed: () {
                    //TODO logic to pop the current context and go back
                  }),
                ],
              ),
            ),
        ),
      );

As for the text editing controller mentioned above, its function is to allow us dynamically get the input in the textfield widget.

And its implementation are as follows

var myController = TextEditingController();
  var myTimeController = TextEditingController();
  var url = "null";
  var d = Data(text: "null", seconds: 0);

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

  @override
  void dispose() {
    myController.dispose();
    super.dispose();
  }

In the above code piece, there is one variable data. It is a self-defined variable to contain our data.

class Data {
  String text;
  int seconds;
  Data({this.text, this.seconds});
}

By setting up this, you get a controller that listen to the input in TextField. And you can get the text/input by 'Mycontroller.text'.

In the button above, you used 'handleUrl()', to handle the input url when user clicked ok.

handleUrl() async {
    var urlPattern = r"(https?|ftp)://([-A-Z0-9.]+)(/[-A-Z0-9+&@#/%=~_|!:,.;]*)?(\?[A-Z0-9+&@#/%=~_|!:‌​,.;]*)?";
    RegExp regExp = new RegExp(urlPattern, caseSensitive: false, multiLine: false,);
    var result = regExp.hasMatch(myController.text);
    if(!result){
      Navigator.pop(context);
      return showDialog<void>(
        context: context,
        barrierDismissible: false, // user must tap button!
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text('Invalid URL'),
            content: SingleChildScrollView(
              child: ListBody(
                children: <Widget>[
                  Text('Please enter a new one'),
                ],
              ),
            ),
            actions: <Widget>[
              FlatButton(
                child: Text('OK'),
                onPressed: () {
                  Navigator.pop(context, d);
                },
              ),
            ],
          );
        },
      );
    }
    else{
      url = myController.text;
      d.text = url;
      int timeInterval = int.parse(myTimeController.text);
      d.seconds = timeInterval;
      Navigator.pop(context, d);
    }
  }

Since for handling button clicking is waiting for the future: onPressed, it’s a async function.

In handle url, you used RegExp to first verify if the input is in correct format.

If not correct, you show up a alertdialog, stating the error and nothing happens.

Otherwise, you put the input, the url and time interval that you got from user, into the class member d, which in the type Data as implemented below.

So passing the input data to d, allows us to get the user input in MyMainPage class, by calling the function getUserInput(), which get the state class member 'd'. Thus you got the input from the user.

And now you are back into the MyMainPage, with input get from the alertDialog.

8.5. MyMainPage, Creating Section

In the asycn function you used above, after you get the input data, you pass it to another function. The function changes the state of the Main Page Widget. This will trigger the main page to recompile with the updated data. Then, you can add one section for the input data into the list view, while waiting for more data and more section.

var urls = new List<Data>();

inputPage(BuildContext context, Widget page) async {
    final dataFromInputPage = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => page),
    ) as Data;

    var data = new Data(seconds: dataFromInputPage.seconds, text: dataFromInputPage.text);

    if(data.text!="null"){
      addNewSection(data);
    }
  }

  addNewSection(Data input){
    setState(() {
      //pass Data and add input
      urls.add(input);
      });
  }

So after the data is added, the following code will run again. This way, the listview in our main page get updated.

@override
  Widget build(BuildContext context) {
     var sections =
        new List.generate(urls.length, (int i) => new Section(input: urls[i]));

8.6. Section

And finally, here you will focus on the main part: For one url in one section, how you interact with the server and show the corresponding results.

First and foremost, Section is a stateful widget which you need to create with specific parameter. Therefore, you put a self-defined constructor for it.

class Section extends StatefulWidget{
  final Data input;

  const Section ({ Key key, this.input }): super(key: key);

  @override
  _SectionState createState() => new _SectionState();
}

class _SectionState extends State<Section> {
...

And the main layout for the section will be as follows.

Here you used one button for the starter of the function.

Once clicked, it calls _press() function to start the interaction with the server.

  Widget build(BuildContext context) {
    return new Container(
      color: Colors.blueGrey,
      child: Column(
        children: <Widget>[
        Text(
          widget.input.text,
          style: TextStyle(fontSize: 20),
        ),
        new Row(
          children: <Widget>[
            new Container(
            padding: new EdgeInsets.all(0.0),
            child: new IconButton(
              icon: (_isOnline
                  ? new Icon(Icons.star)
                  : new Icon(Icons.star_border)),
              color: Colors.red[500],
              onPressed: () => _press(widget.input.text),
            ),
          ),
          new AnimatedContainer(
            height: 20,
            color: Colors.blueGrey,
            duration: Duration(seconds:10),
            child: Text(
              (_totalOnline).toString()+'/'+_totalCheck.toString(),
            style: TextStyle(fontSize: 20),),
            ),
          ],),
        ],),
    );

When _press() is called, it starts this function, fetchStatus() under the timer. The timer is set using flutter´s time function, which allows one function to be executed continously. And for fetchStatus function, you use HTTP package from flutter. And for each response, you call respective function that changes the state of the Section Widget, updating the data.

_press(String url){
  if(!_on){
  _on = true;
  Duration secin = Duration(seconds: widget.input.seconds);
  new Timer.periodic(secin, (Timer t) {
    fetchStatus(url);
    });
  }
}

Future fetchStatus(String url) async {
  final response = await http.get(url);
  if(response.statusCode ==  200){
    _online();
  }
  else{
    _offline();
  }
}

Eventually, you get one functioning app where users can see the status of the servers connected to the input urls.

9. Links and Literature