Building Cryptocurrency Pricing App with Flutter

Building Cryptocurrency Pricing App with Flutter

Building Cryptocurrency Pricing App with Flutter. Flutter allows you to build beautiful native apps on iOS and Android from a single codebase

I’ve come across Google’s new open-source toolkit for cross-platform development recently, and decided to give it a try. You can read about the various benefits here (spoiler, it seems really great), but I thought I would share with you on how to get started with Flutter with something other than their tutorial.

We will be creating a Cryptocurrency Price List app with pull-to-refresh functionality and also to keep track of your favourite Cryptocurrencies.

Prerequisites

I will be using a Macbook with iOS simulator to build this app. Feel free to use Windows machine and an Android emulator.

Before we begin, head over to Flutter and install Flutter and various dependencies. While you are at it, grab your favourite IDE and set it up to use with flutter (we will be using VS Code in this tutorial).

Let’s get started!

In VS Code (follow steps here if needed), search for Flutter New Project to create a new flutter project. Name it crypto_list and let flutter initialise the project for us.

A file main.dart is created, and this will be the point of entry for our app. We will be building the app from scratch, so go ahead and delete all the code there.

This is how your IDE should look at this point.

Creating basic UI
import 'package:flutter/material.dart';

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

//Stateless Widget since this class has no state, once created will be immutable
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //A convenience widget that wraps a number of widgets that are commonly 
    //required for applications implementing Material Design.
    return MaterialApp( 
        title: 'Crypto Price List',
        theme: new ThemeData(primaryColor: Colors.white), //will be used later
        home: new Center(child: new Text('my crypto app'),)); //centralize the text 
  }
}

main.dart

When you start the app, the code will be executed from the main function. So let’s go ahead and create it as above.

open -a Simulator
flutter run

This opens the default simulator used on your machine, and runs the application. Run it and it should show the following

Ok great! You have build your first UI using flutter! It is quite ugly now so let’s create couple of widgets to beautify our app.

Adding widgets
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //material app widget
    return MaterialApp(
      title: 'Crypto Price List',
      theme: new ThemeData(primaryColor: Colors.white),
      home: CryptoList(),
    ); //use our widget instead of the text previously
  }
}

//creates a stateful widget (data inside will change once created)
class CryptoList extends StatefulWidget {
  @override
  CryptoListState createState() => CryptoListState();
}

class CryptoListState extends State {
  //will be used later to view favourited cryptos
  void _pushSaved() {}

//build method
  @override
  Widget build(BuildContext context) {
    //Implements the basic Material Design visual layout structure.
    //This class provides APIs for showing drawers, snack bars, and bottom sheets.
    return Scaffold(
        appBar: AppBar(
          title: Text('CryptoList'),
          actions: [
            // will be used to view favourites
            new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
          ],
        ),
        body: new Center(
          // body of the scaffold
          child: new Text('my crypto app'),
        ));
  }
}

main.dart

Stateless widgets are immutable, meaning that their properties can’t change — all values are final

Stateful widgets maintain state that might change during the lifetime of the widget. Implementing a stateful widget requires at least two classes: 1) a StatefulWidget class that creates an instance of 2) a State class. The StatefulWidget class is, itself, immutable, but the State class persists over the lifetime of the widget.

Edit our code to be like what is shown above.

What we have done is:

  1. Add a stateful widget CryptoList which creates its state class CryptoListState
  2. Most of the logic will be stored in CryptoListState and CryptoList just creates its State class
  3. build to describe how the widget will look. Here we are returning Scaffold which creates an appBar with title and icons and body with centralised text.

Press r to reload changes or R to hot restart (and rebuild state)

With Scaffold we have infinitely improved the overall UI of our app. Remember we used theme in the MyApp class? It was used to set the primary colour of our app, (and by extension appBar's colour).

Feel free to change it accordingly, for eg new ThemeData(primaryColor: Colors.orange) but we will continue using white.

Making HTTP request to get data

Before we add a list of Cryptocurrencies into the body of our view, let’s first get all the data required for us to continue.

import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:http/http.dart' as http;

Import all these modules we will be using at the top of your main.dart file. Find the file pubspec.yaml and http: ^0.12.0+1 (which is required to make http requests) so that it looks like this:

  1. pubspec is where the packages metadata is stored
  2. dart:async allows writing asynchronous code using Future class
  3. dart:convert allows decoding JSON response
  4. package:http/http.dart allows making http requests
  5. dart:math and dart:code provides some helper functions to manipulate data
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:http/http.dart' as http;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //material app widget
    return MaterialApp(
      title: 'Crypto Price List',
      theme: new ThemeData(primaryColor: Colors.white),
      home: CryptoList(),
    ); //use our widget instead of the text previously
  }
}

//creates a stateful widget (data inside will change once created)
class CryptoList extends StatefulWidget {
  @override
  CryptoListState createState() => CryptoListState();
}

class CryptoListState extends State {
  List _cryptoList;
  //this means that the function will be executed sometime in the future (in this case does not return data)
  Future getCryptoPrices() async {
    //async to use await, which suspends the current function, while it does other stuff and resumes when data ready
    print('getting crypto prices'); //print
    String _apiURL =
        "https://api.coinmarketcap.com/v1/ticker/"; //url to get data
    http.Response response = await http.get(_apiURL); //waits for response
    setState(() {
      this._cryptoList =
          jsonDecode(response.body); //sets the state of our widget
      print(_cryptoList); //prints the list
    });
    return;
  }

  @override
  void initState() { //override creation of state so that we can call our function
    super.initState();
    getCryptoPrices(); //this function is called which then sets the state of our app
  }

//build method
  @override
  Widget build(BuildContext context) {
    //Implements the basic Material Design visual layout structure.
    //This class provides APIs for showing drawers, snack bars, and bottom sheets.
    return Scaffold(
        appBar: AppBar(
          title: Text('CryptoList'),
          actions: [
            // will be used to view favourites
            new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
          ],
        ),
        body: new Center(
          // body of the scaffold
          child: new Text('my crypto app'),
        ));
  }

  //will be used later to view favourited cryptos
  void _pushSaved() {}
}

main.dart

Update the code as shown above. Please read the comments on what is added. Essentially, we have asked our widget to execute getCryptoPrices when the state is initialized. We then perform an async operation to get the data from an api, and set state of cryptoList.

Perform hot restart by pressing R

Great! Our data is available for us to use. We can now generate UI to show this data.

Creating ListView
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:http/http.dart' as http;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //material app widget
    return MaterialApp(
      title: 'Crypto Price List',
      theme: new ThemeData(primaryColor: Colors.white),
      home: CryptoList(),
    ); //use our widget instead of the text previously
  }
}

//creates a stateful widget (data inside will change once created)
class CryptoList extends StatefulWidget {
  @override
  CryptoListState createState() => CryptoListState();
}

class CryptoListState extends State {
  List _cryptoList; //store cryptolist
  final _saved = Set(); //store favourited cryptos
  final _boldStyle =
      new TextStyle(fontWeight: FontWeight.bold); //bold text style to be reused
  bool _loading = false; //will be used later to control state
  final List _colors = [
    //to show different colors for different cryptos
    Colors.blue,
    Colors.indigo,
    Colors.lime,
    Colors.teal,
    Colors.cyan
  ];
  //this means that the function will be executed sometime in the future (in this case does not return data)
  Future getCryptoPrices() async {
    //async to use await, which suspends the current function, while it does other stuff and resumes when data ready
    print('getting crypto prices'); //print
    String _apiURL =
        "https://api.coinmarketcap.com/v1/ticker/"; //url to get data
    http.Response response = await http.get(_apiURL); //waits for response
    setState(() {
      this._cryptoList =
          jsonDecode(response.body); //sets the state of our widget
      print(_cryptoList); //prints the list
    });
    return;
  }

  //takes in an object and returns the price with 2 decimal places
  String cryptoPrice(Map crypto) {
    int decimals = 2;
    int fac = pow(10, decimals);
    double d = double.parse(crypto['price_usd']);
    return "\$" + (d = (d * fac).round() / fac).toString();
  }

  // takes in an object and color and returns a circle avatar with first letter and required color
  CircleAvatar _getLeadingWidget(String name, MaterialColor color) {
    return new CircleAvatar(
      backgroundColor: color,
      child: new Text(name[0]),
    );
  }

  @override
  void initState() {
    //override creation of state so that we can call our function
    super.initState();
    getCryptoPrices(); //this function is called which then sets the state of our app
  }

//build method
  @override
  Widget build(BuildContext context) {
    //Implements the basic Material Design visual layout structure.
    //This class provides APIs for showing drawers, snack bars, and bottom sheets.
    return Scaffold(
        appBar: AppBar(
          title: Text('CryptoList'),
          actions: [
            // will be used to view favourites
            new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
          ],
        ),
        body: _buildCryptoList());
  }

  //will be used later to view favourited cryptos
  void _pushSaved() {}

  //widget that builds the list
  Widget _buildCryptoList() {
    return ListView.builder(
        itemCount: _cryptoList
            .length, //set the item count so that index won't be out of range
        padding:
            const EdgeInsets.all(16.0), //add some padding to make it look good
        itemBuilder: (context, i) {
          //item builder returns a row for each index i=0,1,2,3,4
          // if (i.isOdd) return Divider(); //if index = 1,3,5 ... return a divider to make it visually appealing

          // final index = i ~/ 2; //get the actual index excluding dividers.
          final index = i;
          print(index);
          final MaterialColor color = _colors[index %
              _colors.length]; //iterate through indexes and get the next colour
          return _buildRow(_cryptoList[index], color); //build the row widget
        });
  }

  Widget _buildRow(Map crypto, MaterialColor color) {
    // if _saved contains our crypto, return true
    final bool favourited = _saved.contains(crypto);

    // function to handle when heart icon is tapped
    void _fav() {
      setState(() {
        if (favourited) {
          //if it is favourited previously, remove it from the list
          _saved.remove(crypto);
        } else {
          _saved.add(crypto); //else add it to the array
        }
      });
    }

    // returns a row with the desired properties
    return ListTile(
      leading: _getLeadingWidget(crypto['name'],
          color), // get the first letter of each crypto with the color
      title: Text(crypto['name']), //title to be name of the crypto
      subtitle: Text(
        //subtitle is below title, get the price in 2 decimal places and set style to bold
        cryptoPrice(crypto),
        style: _boldStyle,
      ),
      trailing: new IconButton(
        //at the end of the row, add an icon button
        // Add the lines from here...
        icon: Icon(favourited
            ? Icons.favorite
            : Icons
                .favorite_border), // if button is favourited, show favourite icon
        color:
            favourited ? Colors.red : null, // if button is favourited, show red
        onPressed: _fav, //when pressed, let _fav function handle
      ),
    );
  }
}

main.dart

Update your code with the one shown above. A lot of codes have been added, but I have tried to add as much comments as possible to make it understandable. Here is what we have done:

  1. Replace our Scaffold body with _buildCryptoList. This returns a ListView. Rows are build using _buildRow
  2. _buildRow takes in the each cryptocurrency object with colour. It builds a row accordingly: Avatar, Title, Subtitle and Icon
  3. _fav handles when the heart icon is clicked. _saved contains the list of crypto objects we have favourited.

Perform hot restart by pressing R

Great! You have now built an extremely decent looking UI that contains real data about various Cryptocurrencies. If you could not understand the code, it is okay! Take your time and with enough practice, you will get the hang of it. Else, feel free to leave a comment below.

Adding Loading Bar

If you noticed, when you restart the app, there is a brief second where an error is displayed. This is because when the getCryptoPrices is called, the _cryptoList state, which is used to build the list view, is not set, and thus you get an error until the state is set. Let’s edit the code to show an loading bar until _cryptoList is set.

import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'package:http/http.dart' as http;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //material app widget
    return MaterialApp(
      title: 'Crypto Price List',
      theme: new ThemeData(primaryColor: Colors.white),
      home: CryptoList(),
    ); //use our widget instead of the text previously
  }
}

//creates a stateful widget (data inside will change once created)
class CryptoList extends StatefulWidget {
  @override
  CryptoListState createState() => CryptoListState();
}

class CryptoListState extends State {
  List _cryptoList; //store cryptolist
  final _saved = Set(); //store favourited cryptos
  final _boldStyle =
      new TextStyle(fontWeight: FontWeight.bold); //bold text style to be reused
  bool _loading = false; //will be used later to control state
  final List _colors = [
    //to show different colors for different cryptos
    Colors.blue,
    Colors.indigo,
    Colors.lime,
    Colors.teal,
    Colors.cyan
  ];
  //this means that the function will be executed sometime in the future (in this case does not return data)
  Future getCryptoPrices() async {
    //async to use await, which suspends the current function, while it does other stuff and resumes when data ready
    print('getting crypto prices'); //print
    String _apiURL =
        "https://api.coinmarketcap.com/v1/ticker/"; //url to get data
    setState(() {
      this._loading = true; //before calling the api, set the loading to true
    });
    http.Response response = await http.get(_apiURL); //waits for response
    setState(() {
      this._cryptoList =
          jsonDecode(response.body); //sets the state of our widget
      this._loading = false; //set the loading to false after we get a response
      print(_cryptoList); //prints the list
    });
    return;
  }

  //takes in an object and returns the price with 2 decimal places
  String cryptoPrice(Map crypto) {
    int decimals = 2;
    int fac = pow(10, decimals);
    double d = double.parse(crypto['price_usd']);
    return "\$" + (d = (d * fac).round() / fac).toString();
  }

  // takes in an object and color and returns a circle avatar with first letter and required color
  CircleAvatar _getLeadingWidget(String name, MaterialColor color) {
    return new CircleAvatar(
      backgroundColor: color,
      child: new Text(name[0]),
    );
  }

  _getMainBody() {
    if (_loading) { //return progress indicator if it is loading
      return new Center(
        child: new CircularProgressIndicator(),
      );
    } else {
      return _buildCryptoList(); //return the list view
    }
  }

  @override
  void initState() {
    //override creation of state so that we can call our function
    super.initState();
    getCryptoPrices(); //this function is called which then sets the state of our app
  }

//build method
  @override
  Widget build(BuildContext context) {
    //Implements the basic Material Design visual layout structure.
    //This class provides APIs for showing drawers, snack bars, and bottom sheets.
    return Scaffold(
        appBar: AppBar(
          title: Text('CryptoList'),
          actions: [
            // will be used to view favourites
            new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
          ],
        ),
        body: _getMainBody());
  }

  //will be used later to view favourited cryptos
  void _pushSaved() {}

  //widget that builds the list
  Widget _buildCryptoList() {
    return ListView.builder(
        itemCount: _cryptoList
            .length, //set the item count so that index won't be out of range
        padding:
            const EdgeInsets.all(16.0), //add some padding to make it look good
        itemBuilder: (context, i) {
          //item builder returns a row for each index i=0,1,2,3,4
          // if (i.isOdd) return Divider(); //if index = 1,3,5 ... return a divider to make it visually appealing

          // final index = i ~/ 2; //get the actual index excluding dividers.
          final index = i;
          print(index);
          final MaterialColor color = _colors[index %
              _colors.length]; //iterate through indexes and get the next colour
          return _buildRow(_cryptoList[index], color); //build the row widget
        });
  }

  Widget _buildRow(Map crypto, MaterialColor color) {
    // if _saved contains our crypto, return true
    final bool favourited = _saved.contains(crypto);

    // function to handle when heart icon is tapped
    void _fav() {
      setState(() {
        if (favourited) {
          //if it is favourited previously, remove it from the list
          _saved.remove(crypto);
        } else {
          _saved.add(crypto); //else add it to the array
        }
      });
    }

    // returns a row with the desired properties
    return ListTile(
      leading: _getLeadingWidget(crypto['name'],
          color), // get the first letter of each crypto with the color
      title: Text(crypto['name']), //title to be name of the crypto
      subtitle: Text(
        //subtitle is below title, get the price in 2 decimal places and set style to bold
        cryptoPrice(crypto),
        style: _boldStyle,
      ),
      trailing: new IconButton(
        //at the end of the row, add an icon button
        // Add the lines from here...
        icon: Icon(favourited
            ? Icons.favorite
            : Icons
                .favorite_border), // if button is favourited, show favourite icon
        color:
            favourited ? Colors.red : null, // if button is favourited, show red
        onPressed: _fav, //when pressed, let _fav function handle
      ),
    );
  }
}

main.dart

Update the code with this. Instead of directly calling the _buildCryptoList in the body of our Scaffold we are calling another function _getMainBody.

In getCryptoPrices we have also set _loadingto true before we make a request and _loadingto false after have completed the request. _getMainBody then checks and returns a progress bar if _loading is true.

When you run the app, you should see a circular progress indicator for a split second instead of an error page we saw earlier.

Pull to Refresh

We have a decently working app right now. Since I have had prior experience building iOS apps, I wanted to see how it would be to mimic some functionalities. One of the most commonly used functionalities is Pull to Refresh.

Turns out, it is actually very simple.

 _getMainBody() {
    if (_loading) {
      return new Center(
        child: new CircularProgressIndicator(),
      );
    } else {
      return new RefreshIndicator(
        child: _buildCryptoList(),
        onRefresh: getCryptoPrices,
      );
    }
  }

main.dart

Replace the _getMainBody with the above function. What it does is, instead of returning ListView directly, it wraps it around RefreshIndicator which allows pull to refresh possible. We then point the onRefresh to the function where we make the api call.

This is why we needed our widget to be Stateful. When the data is changed, either through polling or serve pushes, we want the data to change and UI to be rendered accordingly. If we just want to display the data once for the duration of the application, we can make our widget to be Stateless.

Pushing New View

We have mentioned earlier that we want to be able to view the favourited Cryptocurrencies. So let’s implement that.

//called when the button is pressed to go to the next view
  void _pushSaved() {
    Navigator.of(context).push( //get the current navigator
      new MaterialPageRoute( //A modal route that replaces the entire screen with a platform-adaptive transition.
        builder: (BuildContext context) {
          final Iterable tiles = _saved.map( //iterate through our saved cryptocurrencies sequentially
            (crypto) {
              return new ListTile( //same list tile as what we have shown in the previous page
                leading: _getLeadingWidget(crypto['name'], Colors.blue),
                title: Text(crypto['name']),
                subtitle: Text(
                  cryptoPrice(crypto),
                  style: _boldStyle,
                ),
              );
            },
          );
          final List divided = ListTile.divideTiles( //divided tiles allows to insert the dividers for visually pleasing outcome
            context: context,
            tiles: tiles,
          ).toList();
          return new Scaffold( //return a new scaffold with a new appbar and listview as a body
            appBar: new AppBar(
              title: const Text('Saved Cryptos'),
            ),
            body: new ListView(children: divided),
          );
        },
      ),
    );
  }

main.dart

Implement _pushSaved function we have declared previously as above. I’ve written in comments what is happening. Basically we get each saved item and build a new list and pass it to the new route’s Scaffold.

This function is triggered when:

new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),

in the home screen. Let’s run the app and see what happens.

And of course, cross platform framework would not be useful if we cannot run it on both Android and iOS! So here are the Android equivalent screenshots with no code change.

I believe certain customisations are definitely possible to better suit the native looks of each platforms, such as by using cupertino style widgets but that is not covered in this tutorial.

Follow

Final Thoughts

This was my first project I have made using Flutter. I have referenced the original sample shown in the official Flutter page, but have extended it to facilitate my own learning. The framework really feels cool, so I might write other complex tutorials in the future.

30s ad

The Complete Flutter App Development Course for Android, iOS

The Complete Flutter and Firebase Developer Course

Dart 2 Complete Bootcamp - Go Hero from Zero in Dart Flutter

Flutter - Firebase - CRUD - Build 2 Apps super easy!

Dart Masterclass Programming Course: iOS/Android Bible

Flutter Course - Learn Flutter From Scratch

Flutter Course - Learn Flutter From Scratch

Flutter Course - Learn Flutter From Scratch - This tutorial series is for everyone who wants to get started with Flutter

Flutter™ is Google’s UI toolkit for building building applications for mobile, web, and desktop from one single codebase. Flutter relies on the dart programming language and uses a compiler for natively building applications for the various target platforms.

This tutorial series is for everyone who wants to get started with Flutter. No prior knowledge is required as we’ll go through every step which is needed to build Flutter application in detail.

1. Flutter Course - Learn Flutter From Scratch: Setting Up The Development Environment

This first episode is about setting up the development environment for Flutter and Dart and running your first Flutter application. Let’s get started:

2. Flutter Course - Learn Flutter From Scratch: Project Structure

In this second part we’re going to dive deeper into the project structure so that you’ll be able to understand what the Flutter project is consisting of.

Testing Flutter UI with Flutter Driver

Testing Flutter UI with Flutter Driver

In this article, I am going to discuss how do we make use of Flutter Driver to test basic user interaction with the app.

In this article, I am going to discuss how do we make use of Flutter Driver to test basic user interaction with the app.

Flutter provides various classes to test user interface of an app

Mobile apps have become an important part of everyone’s life. Be it a Twitter, Facebook, Instagram, most of the people would be browsing one app or another.

Mobile Developers implement features in an app and testing their code is limited to writing unit tests which usually is testing a particular method or a class, basically non-UI testing. But, when it comes to full fledged end to end testing for the app or a new feature, the dedicated QEs need to write automated tests to validate correct functioning of the flow. For instance, how the app responds to a tap, scroll, input, navigation and other interactions. Take any app for instance and just think about what are the most common or basic steps we perform while browsing any app.

Is it a tap ? Yes. In order to like a post or if you want to comment on a post, first action you perform is to tap on that widget (We are talking about Flutter, so it has to be about widget, correct?)

Is it scroll ? Yes. We scroll to keep reading an article, or to see all the tweets on our timeline.

Search for a keyword or person ? Yes. We type a name or a keyword and then press search to see the search results.

Flutter provides various classes to test user interface of an app. Flutter Driver class is one from the pack that helps to drive the application in another process (read: instrumented app) and exposes different helpful methods to test user interaction and interface of the app.

What is Flutter Driver ?

In simple terms, if you have read or used various testing frameworks for web or mobile platforms such as Selenium WebDriver, Protractor for AngularJS and Google Espresso for Android, on similar lines, Flutter Driver is for Flutter. You can read more about Flutter Driver here.

Demo app

we’ll use a simple input textfield followed by a button at center of the screen and will validate functioning of these two widgets as a flow using Flutter driver methods.

Simple UI under test

Flutter Driver setup

Before we could start using Flutter Driver class, we need to make 2 new test files under a predefined folder named test_driver. First file will contain the method that enables Flutter driver extension followed by calling the function which contains the widgets we need to test. So, the first file, let’s name it as app.dart will look like this:

void main() {
  // This line enables the extension
  enableFlutterDriverExtension();

  // Call the `main()` function of your app or call `runApp` with any widget you
  // are interested in testing.
  app.main();
}

The second file will contain the actual test scripts that we are going to write by using driver methods. Let’s name it app_test.dart which will contain methods to connect to Flutter driver and closing the connection once all tests are completed, followed by test scripts.

void main() {
  group('Flutter Driver demo', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        await driver.close();
      }
    });

You can read about the driver setup in more details here.

Note: the above link shows how to add the flutter driver dependency in pubspec.yaml, so I am going to skip that part here.

Now that we have added the required files to connect to Flutter driver, we first need to check the status of the driver extension before writing and executing our test scripts. Enter checkHealth()

checkHealth(): This method verifies the status of Flutter Driver extension we use for our tests. It would be a good practice to execute this method first before we start running tests, because we don’t want our tests to fail due to unavailability or bad status of Flutter Driver.

In app_test.dart, let’s write our first test, as below:

test('check flutter driver health', () async {
  Health health = await driver.checkHealth();
  print(health.status);
});

Very simple method to implement. To see the status, we’ll need to run this test from command line. From terminal, go to the root path of your project and use this command to run our test:

$ flutter drive — target=test_driver/app.dart

00:01 +0: Flutter Driver demo check flutter driver health

HealthStatus.ok

00:01 +1: Flutter Driver demo (tearDownAll)

00:01 +1: All tests passed!

Stopping application instance.

Great ! we are now good to write actual test scripts to validate the UI.

Now, we will write a script to enter a text in input field, validate that the entered text is displayed in the field, update the input field with new text and then validate that the first text entered is not present, followed by tapping on the button and then scrolling to the widget present at the bottom of the screen. For this test, we’ll make use of following methods:

tap()

enterText()

waitFor()

waitForAbsent()

scrollIntoView()

Test script for above scenario will look like this:

test('Flutter drive methods demo', () async {

await driver.tap(find.byValueKey('inputKeyString'));
await driver.enterText('Hello !');
await driver.waitFor(find.text('Hello !'));
await driver.enterText('World');
await driver.waitForAbsent(find.text('Hello !'));
print('World');

await driver.waitFor(find.byValueKey('button'));
await driver.tap(find.byValueKey('button'));
print('Button clicked');

await driver.waitFor(find.byValueKey('text'));
await driver.scrollIntoView(find.byValueKey('text'));
await driver.waitFor(find.text('Scroll till here'));
print('I found you buddy !');

});

Let’s go through each line one by one.

**await** driver.tap(find.byValueKey(**‘inputKeyString’**));

When the instrumented app is launched, the input textfield and button widgets are rendered and in order to insert a text in input field, we first need to tell driver to find the input field and then tap on it to make the input field enabled.

inputKeyString is a key we need to declare in our main.dart file in order to uniquely identify each widget.

await driver.enterText('Hello !');

enterText as its name suggests, helps to enter the text-in-test to input in the given textfield. Since the textfield is now enabled, we instruct driver to enter the text Hello ! in it.

await driver.waitFor(find.text('Hello !'));

waitFor method tells the driver to wait till the given text is found.

Note: Ideally _getText()_ should work with all widgets like _TextField_, _RaisedButton_ and so on, but it seems it currently supports only _Text_ widget. Source: https://github.com/flutter/flutter/issues/16013

await driver.enterText('World');

Above line replaces the first text Hello ! with World

await driver.waitForAbsent(find.text('Hello !'));

waitForAbsent when used, tells driver to wait until the target specified in finder is no longer available.

await driver.waitFor(find.byValueKey('button'));
  await driver.tap(find.byValueKey('button'));

Before we could tap on the button, we first need to find it using find.byValueKey and then tap it.

await driver.waitFor(find.byValueKey('text'));
await driver.scrollIntoView(find.byValueKey('text'));
await driver.waitFor(find.text('Scroll till here'));

Now that we have validated button tap, we’ll need to find the text present at the bottom of the screen. For this, we’ll use scrollIntoView method, but before that we need to instruct driver to find the text widget then scroll into it using key and then validate the text displayed.

Let’s run the script and see the output:

00:01 +0: Flutter Driver demo Flutter drive methods demo

World

Button clicked

I found you buddy !

00:03 +1: Flutter Driver demo (tearDownAll)

00:03 +1: All tests passed!

Stopping application instance.

While running above tests or any instrumented test for that matter, I observed that the execution happens very fast, ie you hardly get to see what events occur on screen while these tests are being executed, so we rely on the command line execution result to see if a test has passed or failed. But it becomes tedious to debug when a test fails or to know why exactly a test failed.

Worry not. Flutter Driver provides another useful method screenshot to capture the screenshots while the tests are being executed. Moreover, we can make use of this method wherever we want throughout a test just like a breakpoint. Having screenshots is a great way to visualize the result and helps to analyze a test failure. Let’s see how to use this method.

screenshot returns a PNG image. Since, we will be calling this method at runtime, driver need to have a path where it can put all the screenshots. For this, we need to create a directory and feed path of this directory to put all screenshots.

Under app_test.dart, inside setupAll() method, enter below line of code, that will create a physical directory named screenshots under your root project.

new Directory(‘screenshots’).create();

Next, we’ll create a custom method that will take the driver instance and path where the image will be stored. First, we convert the png image into integer array and then write that array as bytes into the file path provided. Here’s how:

takeScreenshot(FlutterDriver driver, String path) async {
final List<int> pixels = await driver.screenshot();
final File file = new File(path);
await file.writeAsBytes(pixels);
print(path);

Now, we call this method wherever we want in our test script, as:

await driver.enterText('Hello !');
await takeScreenshot(driver, 'screenshots/entered_text.png');
await driver.waitFor(find.text('Hello !'));
await driver.enterText('World');
await takeScreenshot(driver, 'screenshots/new_text.png');

Let’s run the test script again and see the output:

00:01 +0: Flutter Driver demo Flutter drive methods demo

screenshots/entered_text.png

screenshots/new_text.png

World

Button clicked

I found you buddy !

00:07 +1: Flutter Driver demo (tearDownAll)

00:07 +1: All tests passed!

Stopping application instance.

And we get to see the screenshots created under screenshots folder and the actual images:

With this, we covered various methods Flutter Driver class provides that can be used to test various UI interactions and how these methods can help to write more concise and robust automated tests to validate end-to-end feature flows.

The entire code is available here.

Thanks for reading and feel free to comment below your thoughts or any suggestions / feedback on this article.