JSON Androi parsing JSONArray Ask

I'm trying to parse the JSON :

I'm trying to parse the JSON :

{
    "shopId": 45123,
    "name":"London",
    "desc":"# The city\n## Intro\n**London** is the capital and most populous city of England and the United Kingdom, as well as the most populous municipality in the European Union. Standing on the _River Thames_ in the south east of the island of Great Britain \n## History \n London has been a major settlement for two millennia. It was founded by the Romans, who named it [Londinium](https://en.wikipedia.org/wiki/Londinium). London's ancient core, the City of London, largely retains its 1.12-square-mile (2.9 km2) medieval boundaries.",
    "priceRating": 4.2,
    "qualityRating":5.0,
    "thumbnail":"https://s-media-cache-ak0.pinimg.com/736x/90/63/4e/90634e974743ed9409f527e3b2f24852.jpg",
    "stops": 
    [
        {
            "num": 1,
            "time": "2016-04-27T06:15:00.000Z",
            "title":"Flight to London",
            "desc":"Barcelona BCN-London-Gatwick LGW",
            "type":"0",
            "subtype":0
        },
        {
            "num": 2,
            "time": "2016-04-27T10:35:00.000Z",
            "title":"Gatwick express",
            "desc":"From Airport to London",
            "type":"0",
            "subtype":1
        },
        {
            "num": 3,
            "time": "2016-04-27T12:15:00.000Z",
            "title":"Pub the black horse",
            "desc":"From Airport to London",
            "type":1,
            "subtype":1,
            "location": "51.476334, -0.062700",
            "images": [                 "https://fitzrovianews.files.wordpress.com/2011/01/black_horse_rathbone_pl.jpg" 
            ]               
        },
        {
            "num": 4,
            "time": "2016-04-27T12:16:47.000Z",
            "title":"The Tower Bridge",
            "desc":"# The Tower Bridge Facts\n## Architecture\n**Tower Bridge** is a combined bascule and suspension bridge in London built in _1886–1894_. The bridge crosses the River Thames close to the Tower of London and has become an iconic symbol of London. Tower Bridge is one of five London bridges now owned and maintained by the Bridge House Estates, a charitable trust overseen by the City of London Corporation. \n>It is the only one of the Trust's bridges not to connect the City of London directly to the Southwark bank, as its northern landfall is in Tower Hamlets.\n## The bridge Exhibition\nThis must-see London attraction invites you to step inside the most famous bridge in the world to explore its iconic structure, spectacular views and glass floor, modern exhibitions and magnificent Victorian Engine Rooms! ",
            "type":1,
            "subtype":6,
            "location": "51.507792, -0.087786",
            "images": [                 
    "https://i.ytimg.com/vi/nby0Mr2LfBQ/hqdefault.jpg",
    "http://raindropsofsapphire.com/wp-content/uploads/2011/10/london-bridge.jpg",
    "http://www.londonforfree.net/gizmo/wp-content/uploads/2015/02/southwark-bridge.jpg"
            ]               
        },
        {
            "num": 5,
            "time": "2016-04-27T12:18:10.000Z",
            "title":"St. Paul Cathedral",
            "desc":"# HISTORY \nSt **Paul's Cathedral**, London, is an _Anglican cathedral_, the seat of the _Bishop of London_ and the mother church of the Diocese of London.  \n * It sits on Ludgate Hill at the highest point of the City of London and is a Grade 1 listed building. \n * Its dedication to Paul the Apostle dates back to the original church on this site, founded in AD 604.",
            "type":1,
            "subtype":6,
            "location": "51.513825, -0.098351",
            "images": [                 
    "https://d1wgio6yfhqlw1.cloudfront.net/sysimages/product/resized6/Interior_St_Pauls_Cathedral_132_12992.jpg",
    "https://d1kioxk2jrdjp.cloudfront.net/resized/486x324/48-st_pauls_ctahedral_millenirm_bridge.jpg",
    "http://i4.mirror.co.uk/incoming/article8299330.ece/ALTERNATES/s615b/LOND-2016-052-HMQ-St-Pauls-Thanks-Giving-704JPG.jpg"
            ]               
        }
    ] 


The problem comes when i'm trying to parse the "images" element with the next code :


for (int i = 0; i < stops.length(); i++) {
            JSONObject c = stops.getJSONObject(i);
.....

if(c.has("images") && !c.isNull("images")){
JSONArray imagenes = jsonObj.getJSONArray("images");
for (int j = 0; j < imagenes.length(); j++) {
String valueString= imagenes.getString(i);
Log.d("myTag", "Viatge Parser con valuestring" + valueString);
URL url2 = new URL(valueString);
HttpURLConnection connection = (HttpURLConnection) url2.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
Bitmap miBitmap = BitmapFactory.decodeStream(input);
s.addImage(miBitmap);
}

            }

I'm already comproving if "images" exist with but i keep getting this error:

03-22 00:23:51.391 23180-23797/com.example.usuari.myapplication3 I/System.out: (HTTPLog)-Static: isSBSettingEnabled false
03-22 00:23:51.391 23180-23797/com.example.usuari.myapplication3 I/System.out: (HTTPLog)-Static: isSBSettingEnabled false
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: org.json.JSONException: No value for images
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at org.json.JSONObject.get(JSONObject.java:389)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at org.json.JSONObject.getJSONArray(JSONObject.java:584)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at com.example.usuari.myapplication3.ViatgeParser.parseFromJSON(ViatgeParser.java:97)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at com.example.usuari.myapplication3.DownloadAsyncTask.doInBackground(DownloadAsyncTask.java:38)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at com.example.usuari.myapplication3.DownloadAsyncTask.doInBackground(DownloadAsyncTask.java:17)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at android.os.AsyncTask$2.call(AsyncTask.java:295)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at java.util.concurrent.FutureTask.run(FutureTask.java:237)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
03-22 00:23:51.801 23180-23797/com.example.usuari.myapplication3 W/System.err: at java.lang.Thread.run(Thread.java:818)

I don't understand how can i be getting this error if i already make the proper check in the if statement!

Flutter: Adding Bluetooth Functionality

Flutter: Adding Bluetooth Functionality

This article will help you use Bluetooth functionality with Flutter.

This article will help you use Bluetooth functionality with Flutter.

Introduction:

There is little documentation to no documentation on using Bluetooth in Flutter. In this article, I will help you by demonstrating some basic concepts needed to implement Bluetooth functionality in your app.

Firstly, plugin/dependency we will be using in this app to add Bluetooth is “flutter_bluetooth_serial”, this plugin is implemented from another parent plugin called “flutter_blue”. This is a very new plugin, the only plugin for bluetooth available as of now. It contains a few bugs but trust me, this will surely get your job done for most basic projects.

Note: Before we go any further, it is worth mentioning that this plugin will only work for Android### Implementation:

Add this dependency in your “pubspec.yaml” file :

dependencies:
flutter_bluetooth_serial: ^0.0.4

In the “main.dart” file the base code of the app will look like this:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BluetoothApp(), // BluetoothApp() would be defined later 
    );
  }
}

Now, let’s create a StatefulWidget called “BluetoothApp”. In _BluetoothAppState, we need to define some variables and a Key. We also have to get an instance of FlutterBluetoothSerial in this class. This class will allow us to control and retrieve Bluetooth information.

class BluetoothApp extends StatefulWidget {
  @override
  _BluetoothAppState createState() => _BluetoothAppState();
}

class _BluetoothAppState extends State<BluetoothApp> {
  // Initializing a global key, as it would help us in showing a SnackBar later
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
  // Get the instance of the bluetooth
  FlutterBluetoothSerial bluetooth = FlutterBluetoothSerial.instance;

  // Define some variables, which will be required later
  List<BluetoothDevice> _devicesList = [];
  BluetoothDevice _device;
  bool _connected = false;
  bool _pressed = false;

  @override
  Widget build(BuildContext context) {
    return Container(
      // We have to work on the UI in this part
    );
  }
}

Now, it’s time for implementing the critical portion of the app. We have to get the list of Paired Bluetooth devices and check whether the Bluetooth is connected. This is done asynchronously. We also have to create a list of devices, to be shown in the UI later.

These operations should be done in a “Future” method, which should be called from initState().

class _BluetoothAppState extends State<BluetoothApp> {
  ...

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

  // We are using async callback for using await
  Future<void> bluetoothConnectionState() async {
    List<BluetoothDevice> devices = [];

    // To get the list of paired devices
    try {
      devices = await bluetooth.getBondedDevices();
    } on PlatformException {
      print("Error");
    }

    // For knowing when bluetooth is connected and when disconnected
    bluetooth.onStateChanged().listen((state) {
      switch (state) {
        case FlutterBluetoothSerial.CONNECTED:
          setState(() {
            _connected = true;
            _pressed = false;
          });

          break;

        case FlutterBluetoothSerial.DISCONNECTED:
          setState(() {
            _connected = false;
            _pressed = false;
          });
          break;

        default:
          print(state);
          break;
      }
    });

    // It is an error to call [setState] unless [mounted] is true.
    if (!mounted) {
      return;
    }

    // Store the [devices] list in the [_devicesList] for accessing
    // the list outside this class
    setState(() {
      _devicesList = devices;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      // We have to work on the UI in this part
    );
  }
}

Time to move on to the UI , the most beautiful part of Flutter coding. The code would be a little bit long but it would mostly contain easily readable code, if you are somewhat familiar with the Flutter Widgets. After completing this UI, we have to implement some methods.

...
@override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        key: _scaffoldKey,
        appBar: AppBar(
          title: Text("Flutter Bluetooth"),
          backgroundColor: Colors.deepPurple,
        ),
        body: Container(
          // Defining a Column containing FOUR main Widgets wrapped with some padding:
          // 1. Text
          // 2. Row
          // 3. Card
          // 4. Text (wrapped with "Expanded" and "Padding")
          child: Column(
            mainAxisSize: MainAxisSize.max,
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: Text(
                  "PAIRED DEVICES",
                  style: TextStyle(fontSize: 24, color: Colors.blue),
                  textAlign: TextAlign.center,
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                // Defining a Row containing THREE main Widgets:
                // 1. Text
                // 2. DropdownButton
                // 3. RaisedButton
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    Text(
                      'Device:',
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    DropdownButton(
                      // To be implemented : _getDeviceItems()
                      items: _getDeviceItems(),
                      onChanged: (value) => setState(() => _device = value),
                      value: _device,
                    ),
                    RaisedButton(
                      onPressed:
                          // To be implemented : _disconnect and _connect
                          _pressed ? null : _connected ? _disconnect : _connect, 
                      child: Text(_connected ? 'Disconnect' : 'Connect'),
                    ),
                  ],
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Card(
                  elevation: 4,
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    // Defining a Row containing THREE main Widgets:
                    // 1. Text (wrapped with "Expanded")
                    // 2. FlatButton
                    // 3. FlatButton
                    child: Row(
                      children: <Widget>[
                        Expanded(
                          child: Text(
                            "DEVICE 1",
                            style: TextStyle(
                              fontSize: 20,
                              color: Colors.green,
                            ),
                          ),
                        ),
                        FlatButton(
                          onPressed:
                              // To be implemented : _sendOnMessageToBluetooth()
                              _connected ? _sendOnMessageToBluetooth : null,
                          child: Text("ON"),
                        ),
                        FlatButton(
                          onPressed:
                              // To be implemented : _sendOffMessageToBluetooth()
                              _connected ? _sendOffMessageToBluetooth : null,
                          child: Text("OFF"),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(20),
                  child: Center(
                    child: Text(
                      "NOTE: If you cannot find the device in the list, "
                      "please turn on bluetooth and pair the device by "
                      "going to the bluetooth settings",
                      style: TextStyle(
                          fontSize: 15,
                          fontWeight: FontWeight.bold,
                          color: Colors.red),
                    ),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
}

So, now it’s time for implementing the remaining methods. At first let us start with the _getDeviceItems() method.

  ...
  // Create the List of devices to be shown in Dropdown Menu
  List<DropdownMenuItem<BluetoothDevice>> _getDeviceItems() {
    List<DropdownMenuItem<BluetoothDevice>> items = [];
    if (_devicesList.isEmpty) {
      items.add(DropdownMenuItem(
        child: Text('NONE'),
      ));
    } else {
      _devicesList.forEach((device) {
        items.add(DropdownMenuItem(
          child: Text(device.name),
          value: device,
        ));
      });
    }
    return items;
}

With the UI out of the way, we are left with four methods. For this example, we will be implementing the connect and disconnect methods. We’ll also implement a method to display a “SnackBar” to the user if there are no Bluetooth device is selected when the user tries to connect.

...
// Method to connect to bluetooth
  void _connect() {
    if (_device == null) {
      show('No device selected');
    } else {
      bluetooth.isConnected.then((isConnected) {
        if (!isConnected) {
          bluetooth
              .connect(_device)
              .timeout(Duration(seconds: 10))
              .catchError((error) {
            setState(() => _pressed = false);
          });
          setState(() => _pressed = true);
        }
      });
    }
  }

  // Method to disconnect bluetooth
  void _disconnect() {
    bluetooth.disconnect();
    setState(() => _pressed = true);
  }
  
  // Method to show a Snackbar,
  // taking message as the text
  Future show(
    String message, {
    Duration duration: const Duration(seconds: 3),
  }) async {
    await new Future.delayed(new Duration(milliseconds: 100));
    _scaffoldKey.currentState.showSnackBar(
      new SnackBar(
        content: new Text(
          message,
        ),
        duration: duration,
      ),
    );
  }
...

At this point, we are almost finished. We are now left with two methods, one for sending a message to turn on Bluetooth and the other for sending a message to turn off Bluetooth.

  ...
  // Method to send message,
  // for turning the bletooth device on
  void _sendOnMessageToBluetooth() {
    bluetooth.isConnected.then((isConnected) {
      if (isConnected) {
        bluetooth.write("1");
        show('Device Turned On');
      }
    });
  }

  // Method to send message,
  // for turning the bletooth device off
  void _sendOffMessageToBluetooth() {
    bluetooth.isConnected.then((isConnected) {
      if (isConnected) {
        bluetooth.write("0");
        show('Device Turned Off');
      }
    });
  }
...

That’s it! the Dart code required to make this work is now complete. That said, if we try running our app it will crash:

To fix this, we need to add the sdk to the AndroidManifest. Navigate to your project folder and follow these steps: android -> app -> src -> main -> AndroidManifest.xml

Add these two lines of code in your “AndroidManifest.xml” file :

<manifest ...
    <!-- Add this line (inside manifest tag) -->
    xmlns:tools="http://schemas.android.com/tools">
    
    <!-- and this line (outside manifest tag) -->
    <uses-sdk tools:overrideLibrary="io.github.edufolly.flutterbluetoothserial"/>
    ....

</manifest>

Conclusion:

As I said at the beginning of this article, this plugin contains some bugs and is still under development.

Below are some screenshots showing various phases. If the user doesn’t have permission, the first thing the user will see is a prompt to grant the app location access. This is completely normal, just click “Allow” and everything should be fine.

Screenshots:

You are free to modify the code to add more functionality to the app.

The GitHub repo link for this project is here

If you like this project, please give “Stars” in my GitHub repo. Thank you for reading, if you enjoyed the article make sure to show me some love by hitting that clap button!

Happy coding…

Learn More

Getting started with Flutter

Flutter Tutorial - Flight List UI Example In Flutter

Let’s Develop a Mobile App in Flutter

Mastering styled text in Flutter

A Design Pattern for Flutter

Weather App with “flutter_bloc”

How to integrate your iOS Flutter App with Firebase on MacOS

An introduction to Dart and Flutter

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Mastering styled text in Flutter

Mastering styled text in Flutter

In this tutorial we are going to start with an overview of Dart strings and Unicode. Next we’ll move on to styling text for your app, first for entire strings and then for spans within a string.

Introduction

In this tutorial we are going to start with an overview of Dart strings and Unicode. Next we’ll move on to styling text for your app, first for entire strings and then for spans within a string.

Prerequisites

To go through this tutorial you should have the Flutter development environment set up and know how to run an app. I’m using Android Studio with the Flutter 1.1 plugin, which uses Dart 2.1.

Setup

Create a new Flutter app. I’m calling mine flutter_text.

Open main.dart and replace the code with the following:

    import 'package:flutter/material.dart';

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

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            appBar: AppBar(title: Text('Styling text')),
            body: Container(
              child: Center(
                child: _myWidget(context),
              ),
            ),
          ),
        );
      }
    }

    // modify this widget with the example code below
    Widget _myWidget(BuildContext context) {
      String myString = 'I ❤️ Flutter';
      print(myString);
      return Text(
        myString,
        style: TextStyle(fontSize: 30.0),
      );
    }

Note the _myWidget() function at the end. You can modify or replace it using the examples below. The more you experiment on your own, the more you will learn.

If you are already familiar with concepts like grapheme clusters and Dart strings, you can skip down to the text styling sections below.

Unicode

Coded messages

When I was a kid I liked to write “secret” messages in code, where 1=a, 2=b, 3=c and so on until 26=z. A message using this code might be:

    9   12 9 11 5   6 12 21 20 20 5 18


To make the code even more secret you could shift the numbers, where 1=b, 2=c, 3=d and so on until it wrapped around where 26=a. As long as my friend and I had the same code key, we could decode each other’s messages. The wrong code key, though, would give garbled nonsense.

Computers are similar, except most of the time we don’t want secret messages. We want to make our messages easy to decode, so we agree on a code key, or should I say, a standard. ASCII was an early example of this, where the code key was 97=a, 98=b, 99=c, and so on. That worked fine for English but ASCII only had 128 codes (from 7 bits of data) and that wasn’t enough for all of the characters in other languages. So people made other code keys with more numbers. The problem was that the numbers overlapped and when you used the wrong decoding key you ended up with garbled nonsense.

Unicode to the rescue

Unicode is an international standard that assigns unique code numbers for the characters of every language in the world. The code numbers are called code points. In addition to what we normally think of as characters, there are also code points for control characters (like a new line), diacritical marks (like the accent over an é), and pictures (like 😊). As long as everyone agrees to use this code standard, there are no more fewer garbled messages.

Unicode is just a long list of code points. Saving these code points or sending them is another matter. To help you understand this, take my secret message from above as an example. If I write it as a string of numbers without whitespace and try to send it to you, you get:

    9129115612212020518


This is almost impossible to decode now. Does 912 mean 9, 1, 2 or does it mean 9, 12? It’s the same situation with Unicode. We have to use an agreed upon means to save and send Unicode text, or else it would be very difficult to decode. There are three main ways to do it: UTF-8, UTF-16, and UTF-32. UTF stands for Unicode Transformation Format, and each method of encoding has its advantages and disadvantages.

  • UTF-8 saves each code point using one to four bytes of data.
  • UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
  • UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.

When working with UTF-16 code units, you need to be careful not to forget about the other half of a surrogate pair. And even if you are working with UTF-32, you shouldn’t assume that a single code point is the same as what a user perceives to be a character. For example, country flags (like 🇨🇦) are made of two code points. An accented character (like é) can also optionally be made from two code points. In addition to this, there are emoji with skin tone (like 👩🏾, 2 code points) and family emoji (like 👨‍👩‍👧, 5 code points).

So as a programmer, it is better not to think of UTF code units or Unicode code points as characters themselves. That will lead to bugs (for example, when trying to move the cursor one place to the left). Instead, you should think about what Unicode calls a grapheme cluster. These are user-perceived characters. So 🇨🇦, é, 👩🏾, and 👨‍👩‍👧 are each a single grapheme cluster because they each look like a single character even though they are made up of multiple Unicode code points.

Further reading

If you find this interesting or would like a deeper understand of the issues related to Unicode, I encourage you to read the following articles:

  • UTF-8 saves each code point using one to four bytes of data.
  • UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
  • UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.
Dart strings

Let’s move on from talking about Unicode in a general way to seeing how Dart uses it.

Code units

In Dart, strings are sequences of UTF-16 code units. That makes string manipulation look deceptively easy because you can get the string value of a code unit by a random integer index:

    String myString = 'Flutter';
    String myChar = myString[0]; // F

But this creates bugs if you split a surrogate pair.

    String myString = '🍎';                    // apple emoji
    List<int> codeUnits = myString.codeUnits;  // [55356, 57166]
    String myChar = myString[0];               // 55356 (half of a surrogate pair)

This will throw an exception if you try to display myChar in a Text widget.

Runes

A better alternative is to work with code points, which are called runes in Dart.

    String myString = '🍎π';

    List<int> codeUnits = myString.codeUnits;    // [55356, 57166, 960]
    int numberOfCodeUnits = myString.length;     // 3
    int firstCodeUnit = myString.codeUnitAt(0);  // 55356

    Runes runes = myString.runes;                // (127822, 960)
    int numberOfCodPoints = runes.length;        // 2
    int firstCodePoint = runes.first;            // 127822

Grapheme clusters

Even runes will fail when you have grapheme clusters composed of multiple code points.

    String myString = '🇨🇦';
    Runes runes = myString.runes;                // (127464, 127462)
    int numberOfCodePoints = runes.length;       // 2
    int firstCodePoint = runes.first;            // 127464
    String halfFlag = String.fromCharCode(firstCodePoint); // 🇨

Displaying the halfFlag string in your app won’t crash it, but users will perceive it as a bug since it only contains one of the two regional indicator symbols used to make the Canadian flag.

Unfortunately, at the time of this writing, there is no support for grapheme clusters in Dart, though there is talk of implementing it. You should still keep them in mind while writing tests and working with strings, though.

Hexadecimal notation

If you are starting with a Unicode hex value, this is how you get a string:

    String s1 = '\u0043';                // C
    String s2 = '\u{43}';                // C
    String s3 = '\u{1F431}';             // 🐱 (cat emoji)
    String s4 = '\u{65}\u{301}\u{20DD}'; //  é⃝ = "e" + accent mark + circle
    int charCode = 0x1F431;              // 🐱 (cat emoji)
    String s5 = String.fromCharCode(charCode);

Substrings

The String documentation (here and here) is pretty good, and you should read it if you haven’t already. I want to review substrings before we go on to text styling, though, since we will be using it later.

To get a substring you do the following:

    String myString = 'I ❤️ Flutter.';
    int startIndex = 5;
    int endIndex = 12;
    String mySubstring = myString.substring(startIndex, endIndex); // Flutter

You can find index numbers with indexOf():

    int startIndex = myString.indexOf('Flutter');

OK, that’s enough background information. Let’s get on to styling text in Flutter.

Text styling with the Text widget

We are going to look first at styling strings in a Text widget. After that we will see how to style substrings within a RichText widget. Both of these widgets use a TextStyle widget to hold the styling information.

Replace _myWidget() with the following code:

    Widget _myWidget(BuildContext context) {
      return Text(
        'Styling text in Flutter',
        style: TextStyle(
          fontSize: 30.0,
        ),
      );
    }

Or, if you would like to compare multiple style settings at once, you can use the following column layout.

    Widget _myWidget(BuildContext context) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'Styling text in Flutter',
            style: TextStyle(
              fontSize: 8,
            ),
          ),
          Text(
            'Styling text in Flutter',
            style: TextStyle(
              fontSize: 12,
            ),
          ),
          Text(
            'Styling text in Flutter',
            style: TextStyle(
              fontSize: 16,
            ),
          ),
        ],
      );
    }

Note that I am setting the TextStyle using the style property of the Text widget. I will modify the TextStyle options below. Try them out yourself by pressing hot reload between every change. You may want to leave a large font size (like fontSize: 30) for some of the later examples below so that you can see what is happening.

Text size

    TextStyle(
      fontSize: 30.0,
    )

When fontSize is not given, the default size is 14 logical pixels. Logical pixels are independent of a device’s density. That is, the text should appear to be to be basically the same size no matter what the pixel density of a user’s device may be. However, this font size is also multiplied by a textScaleFactor depending on the user’s preferred font size.

If you wish to disable accessibility scaling, you can set it on the Text widget. (I’m very impressed that Flutter has accessibility enabled by default, and I definitely don’t suggest that you disable it without reason. In some rare cases, though, an oversized font might break a layout…in which case it would still probably be better to redesign your layout rather than disable accessibility.)

    // This text will always display at 30.0 logical pixels, no matter
    // what the user's preferred size is.
    Text(
      'Some text',
      textScaleFactor: 1.0, // disables accessibility
      style: TextStyle(
        fontSize: 30.0
      ),
    )

You can also use the theme data to set the text size. See the section on themes below.

Text color

    TextStyle(
      color: Colors.green,
    )

In addition to predefined colors like Colors.green and Colors.red, you can also set shades on a color, like Colors.blue[100] or Colors.blue[700].

Background color

    Widget _myWidget(BuildContext context) {
      Paint paint = Paint();
      paint.color = Colors.green;
      return Text(
        'Styling text in Flutter',
        style: TextStyle(
          background: paint,
          fontSize: 30.0,
        ),
      );
    }

For a Text widget you could also just wrap it in a Container and set the color on the Container.

Bold

    TextStyle(
      fontWeight: FontWeight.bold,
    )

You can set the weight with numbers like FontWeight.w100 where w400 is the same as normal and w700 is the same as bold.

Italic

    TextStyle(
      fontStyle: FontStyle.italic,
    )

The only choices are italic and normal.

Shadow

    TextStyle(
      shadows: [
        Shadow(
          blurRadius: 10.0,
          color: Colors.blue,
          offset: Offset(5.0, 5.0),
        ),
      ],
    )

When setting the shadow you can change the blur radius (bigger means more blurry), color, and offset. You can even set multiple shadows as if there were more than one light source.

    TextStyle(
      shadows: [
        Shadow(
          color: Colors.blue,
          blurRadius: 10.0,
          offset: Offset(5.0, 5.0),
        ),
        Shadow(
          color: Colors.red,
          blurRadius: 10.0,
          offset: Offset(-5.0, 5.0),
        ),
      ],
    )

I’m not sure if more than one shadow is useful or not, but it is interesting.

Underline

    TextStyle(
      decoration: TextDecoration.underline,
      decorationColor: Colors.black,
      decorationStyle: TextDecorationStyle.solid,
    )

The decoration can be underline, lineThrough, or overline. The last line of text in the image above has an overline.

The choices for decorationStyle are solid, double, dashed, dotted, and wavy.

Spacing

    TextStyle(
      letterSpacing: -1.0,
      wordSpacing: 5.0,
    )

In the example image, the six lines on top use letter spacing ranging from -2.0 to 3.0. The six lines on bottom use word spacing ranging from -3.0 to 12.0. A negative value moves the letters or words closer together.

Font

Using a custom font requires a few more steps:

  1. Add a directory called assets to the root of your project.
  2. Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to dancing_script.ttf.)
  3. In pubspec.yaml register the font:
    flutter:
      fonts:
      - family: DancingScript
        fonts:
        - asset: assets/dancing_script.ttf

  1. Add a directory called assets to the root of your project.
  2. Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to dancing_script.ttf.)
  3. In pubspec.yaml register the font:
    TextStyle(
      fontFamily: 'DancingScript',
    )

  1. Add a directory called assets to the root of your project.
  2. Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to dancing_script.ttf.)
  3. In pubspec.yaml register the font:

See this post for more help.

Using themes

Our root widget is a MaterialApp widget, which uses the Material Design theme. Through the BuildContext we have access to its predefined text styles. Instead of creating our own style with TextStyle, you can use a default one like this:

    Text(
      'Styling text in Flutter',
      style: Theme.of(context).textTheme.title,
    )

That was the default style for titles. There are many more defaults for other types of text. Check them out:

If a style is not specified, Text uses the DefaultTextStyle. You can use it yourself like this:

    Text(
      'default',
      style: DefaultTextStyle.of(context).style,
    )

DefaultTextStyle gets its style from the build context.

See the documentation for more about using themes.

Text styling with the RichText widget

The final thing I want to teach you is how to style part of a text string. With a Text widget the whole string has the same style. A RichText widget, though, allows us to add TextSpans that include different styles.

Basic example

Replace _myWidget() with the following code:

    Widget _myWidget(BuildContext context) {
      return RichText(
        text: TextSpan(
          // set the default style for the children TextSpans
          style: Theme.of(context).textTheme.body1.copyWith(fontSize: 30),
          children: [
            TextSpan(
                text: 'Styling ',
            ),
            TextSpan(
              text: 'text',
              style: TextStyle(
                color: Colors.blue
              )
            ),
            TextSpan(
                text: ' in Flutter',
            ),
          ]
        )
      );
    }

Note: An alternate way to make text with styled spans is to use the Text.rich() constructor, which has the same default style as the Text widget.
RichText takes a TextSpan tree. Every very TextSpan takes more TextSpan children, which inherit the style of their parent. To make the word “text” blue, I had to divide the string into three TextSpans. I used a color for the style, but I could have just as easily used any of the other styles that we have already looked at. Try adding a few more styles yourself.

Styling programmatically

In a real application we would probably have a longer string. For example, let’s highlight every occurrence of “text” in the following string:

To do that we have to look at the string and find the indexes of the text that we want to style. Then we use substring to cut the string up and put it in a list of TextSpans.

Replace _myWidget() with the following code:

    Widget _myWidget(BuildContext context) {

      final String myString =
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter '
          'Styling text in Flutter Styling text in Flutter ';

      final wordToStyle = 'text';
      final style = TextStyle(color: Colors.blue);
      final spans = _getSpans(myString, wordToStyle, style);

      return RichText(
        text: TextSpan(
          style: Theme.of(context).textTheme.body1.copyWith(fontSize: 30),
          children: spans,
        ),
      );
    }

    List<TextSpan> _getSpans(String text, String matchWord, TextStyle style) {

      List<TextSpan> spans = [];
      int spanBoundary = 0;

      do {

        // look for the next match
        final startIndex = text.indexOf(matchWord, spanBoundary);

        // if no more matches then add the rest of the string without style
        if (startIndex == -1) {
          spans.add(TextSpan(text: text.substring(spanBoundary)));
          return spans;
        }

        // add any unstyled text before the next match
        if (startIndex > spanBoundary) {
          spans.add(TextSpan(text: text.substring(spanBoundary, startIndex)));
        }

        // style the matched text
        final endIndex = startIndex + matchWord.length;
        final spanText = text.substring(startIndex, endIndex);
        spans.add(TextSpan(text: spanText, style: style));

        // mark the boundary to start the next search from
        spanBoundary = endIndex;

      // continue until there are no more matches
      } while (spanBoundary < text.length);

      return spans;
    }

Experiment with changing the search word and style.

In this example we searched for plain text, but you can also do pattern matching using regular expressions.

Clickable spans

You can make a span clickable by adding a TapGestureRecognizer:

    TextSpan(
      text: spanText,
      style: style,
      recognizer: TapGestureRecognizer()
        ..onTap = () {
          // do something
        },
    )

This would allow you to open a URL, for example, if used along with the url_launcher plugin.

Final notes

Here are a few more related concepts that I didn’t have time or space to cover:

  • UTF-8 saves each code point using one to four bytes of data.
  • UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
  • UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.
Conclusion

Text seems like it should be so simple, but it really isn’t. Language is messy and dealing with it as a programmer can be difficult. Much progress has been made in recent years, though. Unicode has solved a lot of problems. Dart and Flutter also give us a lot of tools to manipulate and style text. I expect to see these tools improve even more in the future.

The source code for this project is available on GitHub.

By the way, in case you were curious but lazy, my secret message was “I like Flutter”.

Flutter Todos Tutorial with “flutter_bloc”

Flutter Todos Tutorial with “flutter_bloc”

In the following tutorial, we’re going to build a Todos App in Flutter using the Bloc Library.

In the following tutorial, we’re going to build a Todos App in Flutter using the Bloc Library. By the time we’re done, our app should look something like this:

Let’s get started!

Setup

We’ll start off by creating a brand new Flutter project

flutter create flutter_todos

We can then replace the contents of pubspec.yaml with:

name: flutter_todos
description: A new Flutter project.

environment:
  sdk: ">=2.0.0 <3.0.0"

dependencies:
  meta: ">=1.1.0 <2.0.0"
  equatable: ^0.2.0
  flutter_bloc: ^0.7.0
  flutter:
    sdk: flutter

dependency_overrides:
  todos_app_core:
    git:
      url: git://github.com/brianegan/flutter_architecture_samples
      path: todos_app_core
  todos_repository_core:
    git:
      url: git://github.com/brianegan/flutter_architecture_samples
      path: todos_repository_core
  todos_repository_simple:
    git:
      url: git://github.com/brianegan/flutter_architecture_samples
      path: todos_repository_simple

flutter:
uses-material-design: true

and finally install all of our dependencies

flutter packages get

Note*: We’re overriding some dependencies because we’re going to be reusing them from* Brian Egan’s Flutter Architecture Samples.

Todos Repository

In this tutorial we’re not going to go into the implementation details of the TodosRepository because it was implemented by Brian Egan and is shared among all of the Todo Architecture Samples.

At a high level, the TodosRepository will expose a method to loadTodos and to saveTodos. That’s pretty much all we need to know so for the rest of the tutorial we’ll focus on the Bloc and Presentation layers.

Todos Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.#### Model

The first thing we need to do is define our Todo model. Each todo will need to have an id, a task, an optional note, and an optional completed flag.

Let’s create a models directory and create todo.dart.

import 'package:todos_app_core/todos_app_core.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:todos_repository_core/todos_repository_core.dart';

@immutable
class Todo extends Equatable {
  final bool complete;
  final String id;
  final String note;
  final String task;

  Todo(this.task, {this.complete = false, String note = '', String id})
      : this.note = note ?? '',
        this.id = id ?? Uuid().generateV4(),
        super([complete, id, note, task]);

  Todo copyWith({bool complete, String id, String note, String task}) {
    return Todo(
      task ?? this.task,
      complete: complete ?? this.complete,
      id: id ?? this.id,
      note: note ?? this.note,
    );
  }

  @override
  String toString() {
    return 'Todo { complete: $complete, task: $task, note: $note, id: $id }';
  }

  TodoEntity toEntity() {
    return TodoEntity(task, id, note, complete);
  }

  static Todo fromEntity(TodoEntity entity) {
    return Todo(
      entity.task,
      complete: entity.complete ?? false,
      note: entity.note,
      id: entity.id ?? Uuid().generateV4(),
    );
  }
}

Note*: We’re using the* Equatable package so that we can compare instances of _Todos_ without having to manually override _==_ and _hashCode_.

Next up, we need to create the TodosState which our presentation layer will receive.

States

Let’s create blocs/todos/todos_state.dart and define the different states we’ll need to handle.

The three states we will implement are:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TodosState extends Equatable {
  TodosState([List props = const []]) : super(props);
}

class TodosLoading extends TodosState {
  @override
  String toString() => 'TodosLoading';
}

class TodosLoaded extends TodosState {
  final List<Todo> todos;

  TodosLoaded([this.todos = const []]) : super([todos]);

  @override
  String toString() => 'TodosLoaded { todos: $todos }';
}

class TodosNotLoaded extends TodosState {
  @override
  String toString() => 'TodosNotLoaded';
}

Note*: We are annotating our base* _TodosState_ with the immutable decorator so that we can indicate that all _TodosStates_ cannot be changed.

Next, let’s implement the events we will need to handle.

Events

The events we will need to handle in our TodosBloc are:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

Create blocs/todos/todos_event.dart and let’s implement the events we described above.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TodosEvent extends Equatable {
  TodosEvent([List props = const []]) : super(props);
}

class LoadTodos extends TodosEvent {
  @override
  String toString() => 'LoadTodos';
}

class AddTodo extends TodosEvent {
  final Todo todo;

  AddTodo(this.todo) : super([todo]);

  @override
  String toString() => 'AddTodo { todo: $todo }';
}

class UpdateTodo extends TodosEvent {
  final Todo updatedTodo;

  UpdateTodo(this.updatedTodo) : super([updatedTodo]);

  @override
  String toString() => 'UpdateTodo { updatedTodo: $updatedTodo }';
}

class DeleteTodo extends TodosEvent {
  final Todo todo;

  DeleteTodo(this.todo) : super([todo]);

  @override
  String toString() => 'DeleteTodo { todo: $todo }';
}

class ClearCompleted extends TodosEvent {
  @override
  String toString() => 'ClearCompleted';
}

class ToggleAll extends TodosEvent {
  @override
  String toString() => 'ToggleAll';
}

Now that we have our TodosStates and TodosEvents implemented we can implement our TodosBloc.

Bloc

Let’s create blocs/todos/todos_bloc.dart and get started! We just need to implement initialState and mapEventToState.

Tip*: Check out the* Bloc VSCode Extension which provides tools for effectively creating blocs for both Flutter and AngularDart apps.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';

class TodosBloc extends Bloc<TodosEvent, TodosState> {
  final TodosRepositoryFlutter todosRepository;

  TodosBloc({@required this.todosRepository});

  @override
  TodosState get initialState => TodosLoading();

  @override
  Stream<TodosState> mapEventToState(
    TodosState currentState,
    TodosEvent event,
  ) async* {
    if (event is LoadTodos) {
      yield* _mapLoadTodosToState();
    } else if (event is AddTodo) {
      yield* _mapAddTodoToState(currentState, event);
    } else if (event is UpdateTodo) {
      yield* _mapUpdateTodoToState(currentState, event);
    } else if (event is DeleteTodo) {
      yield* _mapDeleteTodoToState(currentState, event);
    } else if (event is ToggleAll) {
      yield* _mapToggleAllToState(currentState);
    } else if (event is ClearCompleted) {
      yield* _mapClearCompletedToState(currentState);
    }
  }

  Stream<TodosState> _mapLoadTodosToState() async* {
    try {
      final todos = await this.todosRepository.loadTodos();
      yield TodosLoaded(
        todos.map(Todo.fromEntity).toList(),
      );
    } catch (_) {
      yield TodosNotLoaded();
    }
  }

  Stream<TodosState> _mapAddTodoToState(
    TodosState currentState,
    AddTodo event,
  ) async* {
    if (currentState is TodosLoaded) {
      final List<Todo> updatedTodos = List.from(currentState.todos)
        ..add(event.todo);
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapUpdateTodoToState(
    TodosState currentState,
    UpdateTodo event,
  ) async* {
    if (currentState is TodosLoaded) {
      final List<Todo> updatedTodos = currentState.todos.map((todo) {
        return todo.id == event.updatedTodo.id ? event.updatedTodo : todo;
      }).toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapDeleteTodoToState(
    TodosState currentState,
    DeleteTodo event,
  ) async* {
    if (currentState is TodosLoaded) {
      final updatedTodos =
          currentState.todos.where((todo) => todo.id != event.todo.id).toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapToggleAllToState(TodosState currentState) async* {
    if (currentState is TodosLoaded) {
      final allComplete = currentState.todos.every((todo) => todo.complete);
      final List<Todo> updatedTodos = currentState.todos
          .map((todo) => todo.copyWith(complete: !allComplete))
          .toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapClearCompletedToState(TodosState currentState) async* {
    if (currentState is TodosLoaded) {
      final List<Todo> updatedTodos =
          currentState.todos.where((todo) => !todo.complete).toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Future _saveTodos(List<Todo> todos) {
    return todosRepository.saveTodos(
      todos.map((todo) => todo.toEntity()).toList(),
    );
  }
}

When we yield a state in the private mapEventToState handlers, we are always yielding a new state instead of mutating the currentState. This is because every time we yield, bloc will compare the currentState to the nextState and will only trigger a state change (transition) if the two states are not equal. If we just mutate and yield the same instance of state, then currentState == nextState would evaluate to true and no state change would occur.

Our TodosBloc will have a dependency on the TodosRepository so that it can load and save todos. It will have an initial state of TodosLoading and defines the private handlers for each of the events. Whenever the TodosBloc changes the list of todos it calls the saveTodos method in the TodosRepository in order to keep everything persisted locally.

Barrel File

Now that we’re done with our TodosBloc we can create a barrel file to export all of our bloc files and make it convenient to import them later on.

Create blocs/todos/todos.dart and export the bloc, events, and states:

export './todos_bloc.dart';
export './todos_event.dart';
export './todos_state.dart';

Filtered Todos Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.#### Model

Before we start defining and implementing the TodosStates, we will need to implement a VisibilityFilter model that will determine which todos our FilteredTodosState will contain. In this case, we will have three filters:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

We can create models/visibility_filter.dart and define our filter as an enum:

enum VisibilityFilter { all, active, completed }

States

Just like we did with the TodosBloc, we’ll need to define the different states for our FilteredTodosBloc.

In this case, we only have two states:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

Let’s create blocs/filtered_todos/filtered_todos_state.dart and implement the two states.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class FilteredTodosState extends Equatable {
  FilteredTodosState([List props = const []]) : super(props);
}

class FilteredTodosLoading extends FilteredTodosState {
  @override
  String toString() => 'FilteredTodosLoading';
}

class FilteredTodosLoaded extends FilteredTodosState {
  final List<Todo> filteredTodos;
  final VisibilityFilter activeFilter;

  FilteredTodosLoaded(this.filteredTodos, this.activeFilter)
      : super([filteredTodos, activeFilter]);

  @override
  String toString() {
    return 'FilteredTodosLoaded { filteredTodos: $filteredTodos, activeFilter: $activeFilter }';
  }
}

Note*: The* _FilteredTodosLoaded_ state contains the list of filtered todos as well as the active visibility filter.

Events

We’re going to implement two events for our FilteredTodosBloc:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

Create blocs/filtered_todos/filtered_todos_event.dart and let’s implement the two events.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class FilteredTodosEvent extends Equatable {
  FilteredTodosEvent([List props = const []]) : super(props);
}

class UpdateFilter extends FilteredTodosEvent {
  final VisibilityFilter filter;

  UpdateFilter(this.filter) : super([filter]);

  @override
  String toString() => 'UpdateFilter { filter: $filter }';
}

class UpdateTodos extends FilteredTodosEvent {
  final List<Todo> todos;

  UpdateTodos(this.todos) : super([todos]);

  @override
  String toString() => 'UpdateTodos { todos: $todos }';
}

We’re ready to implement our FilteredTodosBloc next!

Bloc

Our FilteredTodosBloc will be similar to our TodosBloc; however, instead of having a dependency on the TodosRepository, it will have a dependency on the TodosBloc itself. This will allow the FilteredTodosBloc to update its state in response to state changes in the TodosBloc.

Create blocs/filtered_todos/filtered_todos_bloc.dart and let’s get started.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';

class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
  final TodosBloc todosBloc;
  StreamSubscription todosSubscription;

  FilteredTodosBloc({@required this.todosBloc}) {
    todosSubscription = todosBloc.state.listen((state) {
      if (state is TodosLoaded) {
        dispatch(UpdateTodos((todosBloc.currentState as TodosLoaded).todos));
      }
    });
  }

  @override
  FilteredTodosState get initialState {
    return todosBloc.currentState is TodosLoaded
        ? FilteredTodosLoaded(
            (todosBloc.currentState as TodosLoaded).todos,
            VisibilityFilter.all,
          )
        : FilteredTodosLoading();
  }

  @override
  Stream<FilteredTodosState> mapEventToState(
    FilteredTodosState currentState,
    FilteredTodosEvent event,
  ) async* {
    if (event is UpdateFilter) {
      yield* _mapUpdateFilterToState(currentState, event);
    } else if (event is UpdateTodos) {
      yield* _mapTodosUpdatedToState(currentState, event);
    }
  }

  Stream<FilteredTodosState> _mapUpdateFilterToState(
    FilteredTodosState currentState,
    UpdateFilter event,
  ) async* {
    if (todosBloc.currentState is TodosLoaded) {
      yield FilteredTodosLoaded(
        _mapTodosToFilteredTodos(
          (todosBloc.currentState as TodosLoaded).todos,
          event.filter,
        ),
        event.filter,
      );
    }
  }

  Stream<FilteredTodosState> _mapTodosUpdatedToState(
    FilteredTodosState currentState,
    UpdateTodos event,
  ) async* {
    final visibilityFilter = currentState is FilteredTodosLoaded
        ? currentState.activeFilter
        : VisibilityFilter.all;
    yield FilteredTodosLoaded(
      _mapTodosToFilteredTodos(
        (todosBloc.currentState as TodosLoaded).todos,
        visibilityFilter,
      ),
      visibilityFilter,
    );
  }

  List<Todo> _mapTodosToFilteredTodos(
      List<Todo> todos, VisibilityFilter filter) {
    return todos.where((todo) {
      if (filter == VisibilityFilter.all) {
        return true;
      } else if (filter == VisibilityFilter.active) {
        return !todo.complete;
      } else if (filter == VisibilityFilter.completed) {
        return todo.complete;
      }
    }).toList();
  }

  @override
  void dispose() {
    todosSubscription.cancel();
    super.dispose();
  }
}

We create a StreamSubscription for the stream of TodosStates so that we can listen to the state changes in the TodosBloc. We override the bloc’s dispose method and cancel the subscription so that we can clean up after the bloc is disposed.

Barrel File

Just like before, we can create a barrel file to make it more convenient to import the various filtered todos classes.

Create blocs/filtered_todos/filtered_todos.dart and export the three files:

export './filtered_todos_bloc.dart';
export './filtered_todos_event.dart';
export './filtered_todos_state.dart';

Stats Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.#### State

Our StatsBloc will have two states that it can be in:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

Create blocs/stats/stats_state.dart and let’s implement our StatsState.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class StatsState extends Equatable {
  StatsState([List props = const []]) : super(props);
}

class StatsLoading extends StatsState {
  @override
  String toString() => 'StatsLoading';
}

class StatsLoaded extends StatsState {
  final int numActive;
  final int numCompleted;

  StatsLoaded(this.numActive, this.numCompleted)
      : super([numActive, numCompleted]);

  @override
  String toString() {
    return 'StatsLoaded { numActive: $numActive, numCompleted: $numCompleted }';
  }
}

Next, let’s define and implement the StatsEvents.

Events

There will just be a single event our StatsBloc will respond to: UpdateStats. This event will be dispatched whenever the TodosBloc state changes so that our StatsBloc can recalculate the new statistics.

Create blocs/stats/states_event.dart and let’s implement it.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class StatsEvent extends Equatable {
  StatsEvent([List props = const []]) : super(props);
}

class UpdateStats extends StatsEvent {
  final List<Todo> todos;

  UpdateStats(this.todos) : super([todos]);

  @override
  String toString() => 'UpdateStats { todos: $todos }';
}

Now we’re ready to implement our StatsBloc which will look very similar to the FilteredTodosBloc.

Bloc

Our StatsBloc will have a dependency on the TodosBloc itself which will allow it to update its state in response to state changes in the TodosBloc.

Create blocs/stats/stats_bloc.dart and let’s get started.

import 'dart:async';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';

class StatsBloc extends Bloc<StatsEvent, StatsState> {
  final TodosBloc todosBloc;
  StreamSubscription todosSubscription;

  StatsBloc({@required this.todosBloc}) {
    todosSubscription = todosBloc.state.listen((state) {
      if (state is TodosLoaded) {
        dispatch(UpdateStats(state.todos));
      }
    });
  }

  @override
  StatsState get initialState => StatsLoading();

  @override
  Stream<StatsState> mapEventToState(
    StatsState currentState,
    StatsEvent event,
  ) async* {
    if (event is UpdateStats) {
      int numActive =
          event.todos.where((todo) => !todo.complete).toList().length;
      int numCompleted =
          event.todos.where((todo) => todo.complete).toList().length;
      yield StatsLoaded(numActive, numCompleted);
    }
  }

  @override
  void dispose() {
    todosSubscription.cancel();
    super.dispose();
  }
}

That’s all there is to it! Our StatsBloc recalculates its state which contains the number of active todos and the number of completed todos on each state change of our TodosBloc.

Now that we’re done with the StatsBloc we just have one last bloc to implement: the TabBloc.

Tab Bloc

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.#### Model / State

We need to define an AppTab model which we will also use to represent the TabState. The AppTab will just be an enum which represents the active tab in our application. Since the app we’re building will only have two tabs: todos and stats, we just need two values.

Create models/app_tab.dart:

enum AppTab { todos, stats }

Event

Our TabBloc will be responsible for handling a single TabEvent:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

Create blocs/tab/tab_event.dart:

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TabEvent extends Equatable {
  TabEvent([List props = const []]) : super(props);
}

class UpdateTab extends TabEvent {
  final AppTab tab;

  UpdateTab(this.tab) : super([tab]);

  @override
  String toString() => 'UpdateTab { tab: $tab }';
}

Bloc

Our TabBloc implementation will be super simple. As always, we just need to implement initialState and mapEventToState.

Create blocs/tab/tab_bloc.dart and let’s quickly do the implementation.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/tab/tab.dart';
import 'package:flutter_todos/models/models.dart';

class TabBloc extends Bloc<TabEvent, AppTab> {
  @override
  AppTab get initialState => AppTab.todos;

  @override
  Stream<AppTab> mapEventToState(
    AppTab currentState,
    TabEvent event,
  ) async* {
    if (event is UpdateTab) {
      yield event.tab;
    }
  }
}

I told you it’d be simple. All the TabBloc is doing is setting the initial state to the todos tab and handling the UpdateTab event by yielding a new AppTab instance.

Barrel File

Lastly, we’ll create another barrel file for our TabBloc exports. Create blocs/tab/tab.dart and export the two files:

export './tab_bloc.dart';
export './tab_event.dart';

Bloc Delegate

Before we move on to the presentation layer, we will implement our own BlocDelegate which will allow us to handle all state changes and errors in a single place. It’s really useful for things like developer logs or analytics.

Create blocs/simple_bloc_delegate.dart and let’s get started.

import 'package:bloc/bloc.dart';

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onTransition(Transition transition) {
    print(transition);
  }

  @override
  void onError(Object error, StackTrace stacktrace) {
    print(error);
  }
}

All we’re doing in this case is printing all state changes (transitions) and errors to the console just so that we can see what’s going on when we’re running our app locally. You can hook up your BlocDelegate to Google Analytics, Sentry, Crashlytics, etc…

Blocs Barrel File

Now that we have all of our blocs implemented we can create a barrel file. Create blocs/blocs.dart and export all of our blocs so that we can conveniently import any bloc code with a single import.

export './filtered_todos/filtered_todos.dart';
export './stats/stats.dart';
export './tab/tab.dart';
export './todos/todos.dart';
export './simple_bloc_delegate.dart';

Up next, we’ll focus on implementing the major screens in our Todos application.

Screens

Home Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create a new directory called screens where we will put all of our new screen widgets and then create screens/home_screen.dart.

Our HomeScreen will be a StatefulWidget because it will need to create and dispose the TabBloc, FilteredTodosBloc, and StatsBloc.

import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/models/models.dart';

class HomeScreen extends StatefulWidget {
  final void Function() onInit;

  HomeScreen({@required this.onInit}) : super(key: ArchSampleKeys.homeScreen);

  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final TabBloc _tabBloc = TabBloc();
  FilteredTodosBloc _filteredTodosBloc;
  StatsBloc _statsBloc;

  @override
  void initState() {
    widget.onInit();
    _filteredTodosBloc = FilteredTodosBloc(
      todosBloc: BlocProvider.of<TodosBloc>(context),
    );
    _statsBloc = StatsBloc(
      todosBloc: BlocProvider.of<TodosBloc>(context),
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
      bloc: _tabBloc,
      builder: (BuildContext context, AppTab activeTab) {
        return BlocProviderTree(
          blocProviders: [
            BlocProvider<TabBloc>(bloc: _tabBloc),
            BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
            BlocProvider<StatsBloc>(bloc: _statsBloc),
          ],
          child: Scaffold(
            appBar: AppBar(
              title: Text(FlutterBlocLocalizations.of(context).appTitle),
              actions: [
                FilterButton(visible: activeTab == AppTab.todos),
                ExtraActions(),
              ],
            ),
            body: activeTab == AppTab.todos ? FilteredTodos() : Stats(),
            floatingActionButton: FloatingActionButton(
              key: ArchSampleKeys.addTodoFab,
              onPressed: () {
                Navigator.pushNamed(context, ArchSampleRoutes.addTodo);
              },
              child: Icon(Icons.add),
              tooltip: ArchSampleLocalizations.of(context).addTodo,
            ),
            bottomNavigationBar: TabSelector(
              activeTab: activeTab,
              onTabSelected: (tab) => _tabBloc.dispatch(UpdateTab(tab)),
            ),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _statsBloc.dispose();
    _filteredTodosBloc.dispose();
    _tabBloc.dispose();
    super.dispose();
  }
}

The HomeScreen creates the TabBloc, FilteredTodosBloc, and StatsBloc as part of its state. It uses BlocProvider.of<TodosBloc>(context) in order to access the TodosBloc which will be made available from our root TodosApp widget (we’ll get to it later in this tutorial).

Since the HomeScreen needs to respond to changes in the TodosBloc state, we use BlocBuilder in order to build the correct widget based on the current TodosState.

The HomeScreen also makes the TabBloc, FilteredTodosBloc, and StatsBloc available to the widgets in its subtree by using the BlocProviderTree widget from flutter_bloc.

BlocProviderTree(
  blocProviders: [
    BlocProvider<TabBloc>(bloc: _tabBloc),
    BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
    BlocProvider<StatsBloc>(bloc: _statsBloc),
  ],
  child: Scaffold(...),
);

is equivalent to writing

BlocProvider<TabBloc>(
  bloc: _tabBloc,
  child: BlocProvider<FilteredTodosBloc>(
    bloc: _filteredTodosBloc,
    child: BlocProvider<StatsBloc>(
      bloc: _statsBloc,
      child: Scaffold(...),
    ),
  ),
);

You can see how using BlocProviderTree helps reduce the levels of nesting and makes the code easier to read and maintain.

Next, we’ll implement the DetailsScreen.

Details Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create screens/details_screen.dart and let’s build it.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class DetailsScreen extends StatelessWidget {
  final String id;

  DetailsScreen({Key key, @required this.id})
      : super(key: key ?? ArchSampleKeys.todoDetailsScreen);

  @override
  Widget build(BuildContext context) {
    final todosBloc = BlocProvider.of<TodosBloc>(context);
    return BlocBuilder(
      bloc: todosBloc,
      builder: (BuildContext context, TodosState state) {
        final todo = (state as TodosLoaded)
            .todos
            .firstWhere((todo) => todo.id == id, orElse: () => null);
        final localizations = ArchSampleLocalizations.of(context);
        return Scaffold(
          appBar: AppBar(
            title: Text(localizations.todoDetails),
            actions: [
              IconButton(
                tooltip: localizations.deleteTodo,
                key: ArchSampleKeys.deleteTodoButton,
                icon: Icon(Icons.delete),
                onPressed: () {
                  todosBloc.dispatch(DeleteTodo(todo));
                  Navigator.pop(context, todo);
                },
              )
            ],
          ),
          body: todo == null
              ? Container(key: FlutterTodosKeys.emptyDetailsContainer)
              : Padding(
                  padding: EdgeInsets.all(16.0),
                  child: ListView(
                    children: [
                      Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Padding(
                            padding: EdgeInsets.only(right: 8.0),
                            child: Checkbox(
                                key: FlutterTodosKeys.detailsScreenCheckBox,
                                value: todo.complete,
                                onChanged: (_) {
                                  todosBloc.dispatch(
                                    UpdateTodo(
                                      todo.copyWith(complete: !todo.complete),
                                    ),
                                  );
                                }),
                          ),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Hero(
                                  tag: '${todo.id}__heroTag',
                                  child: Container(
                                    width: MediaQuery.of(context).size.width,
                                    padding: EdgeInsets.only(
                                      top: 8.0,
                                      bottom: 16.0,
                                    ),
                                    child: Text(
                                      todo.task,
                                      key: ArchSampleKeys.detailsTodoItemTask,
                                      style:
                                          Theme.of(context).textTheme.headline,
                                    ),
                                  ),
                                ),
                                Text(
                                  todo.note,
                                  key: ArchSampleKeys.detailsTodoItemNote,
                                  style: Theme.of(context).textTheme.subhead,
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
          floatingActionButton: FloatingActionButton(
            key: ArchSampleKeys.editTodoFab,
            tooltip: localizations.editTodo,
            child: Icon(Icons.edit),
            onPressed: todo == null
                ? null
                : () {
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (context) {
                          return AddEditScreen(
                            key: ArchSampleKeys.editTodoScreen,
                            onSave: (task, note) {
                              todosBloc.dispatch(
                                UpdateTodo(
                                  todo.copyWith(task: task, note: note),
                                ),
                              );
                            },
                            isEditing: true,
                            todo: todo,
                          );
                        },
                      ),
                    );
                  },
          ),
        );
      },
    );
  }
}

Note*: The* _DetailsScreen_ requires a todo id so that it can pull the todo details from the _TodosBloc_ and so that it can update whenever a todo’s details have been changed (a todo’s id cannot be changed).

The main things to note are that there is an IconButton which dispatches a DeleteTodo event as well as a checkbox which dispatches an UpdateTodo event.

There is also another FloatingActionButton which navigates the user to the AddEditScreen with isEditing set to true. We’ll take a look at the AddEditScreen next.

Add/Edit Screen

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create screens/add_edit_screen.dart and let’s have a look at the implementation.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

typedef OnSaveCallback = Function(String task, String note);

class AddEditScreen extends StatefulWidget {
  final bool isEditing;
  final OnSaveCallback onSave;
  final Todo todo;

  AddEditScreen({
    Key key,
    @required this.onSave,
    @required this.isEditing,
    this.todo,
  }) : super(key: key ?? ArchSampleKeys.addTodoScreen);

  @override
  _AddEditScreenState createState() => _AddEditScreenState();
}

class _AddEditScreenState extends State<AddEditScreen> {
  static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  String _task;
  String _note;

  bool get isEditing => widget.isEditing;

  @override
  Widget build(BuildContext context) {
    final localizations = ArchSampleLocalizations.of(context);
    final textTheme = Theme.of(context).textTheme;

    return Scaffold(
      appBar: AppBar(
        title: Text(
          isEditing ? localizations.editTodo : localizations.addTodo,
        ),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: ListView(
            children: [
              TextFormField(
                initialValue: isEditing ? widget.todo.task : '',
                key: ArchSampleKeys.taskField,
                autofocus: !isEditing,
                style: textTheme.headline,
                decoration: InputDecoration(
                  hintText: localizations.newTodoHint,
                ),
                validator: (val) {
                  return val.trim().isEmpty
                      ? localizations.emptyTodoError
                      : null;
                },
                onSaved: (value) => _task = value,
              ),
              TextFormField(
                initialValue: isEditing ? widget.todo.note : '',
                key: ArchSampleKeys.noteField,
                maxLines: 10,
                style: textTheme.subhead,
                decoration: InputDecoration(
                  hintText: localizations.notesHint,
                ),
                onSaved: (value) => _note = value,
              )
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key:
            isEditing ? ArchSampleKeys.saveTodoFab : ArchSampleKeys.saveNewTodo,
        tooltip: isEditing ? localizations.saveChanges : localizations.addTodo,
        child: Icon(isEditing ? Icons.check : Icons.add),
        onPressed: () {
          if (_formKey.currentState.validate()) {
            _formKey.currentState.save();
            widget.onSave(_task, _note);
            Navigator.pop(context);
          }
        },
      ),
    );
  }
}

There’s nothing bloc-specific in this widget. It’s simply presenting a form and:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

It uses an onSave callback function to notify its parent of the updated or newly created todo.

That’s it for the screens in our application so before we forget, let’s create a barrel file to export them.

Screens Barrel File

Create screens/screens.dart and export all three.

export './add_edit_screen.dart';
export './details_screen.dart';
export './home_screen.dart';

Widgets

FilterButton

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create a new directory called widgets and put our FilterButton implementation in widgets/filter_button.dart.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/models/models.dart';

class FilterButton extends StatelessWidget {
  final bool visible;

  FilterButton({this.visible, Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final defaultStyle = Theme.of(context).textTheme.body1;
    final activeStyle = Theme.of(context)
        .textTheme
        .body1
        .copyWith(color: Theme.of(context).accentColor);
    final FilteredTodosBloc filteredTodosBloc =
        BlocProvider.of<FilteredTodosBloc>(context);
    return BlocBuilder(
        bloc: filteredTodosBloc,
        builder: (BuildContext context, FilteredTodosState state) {
          final button = _Button(
            onSelected: (filter) {
              filteredTodosBloc.dispatch(UpdateFilter(filter));
            },
            activeFilter: state is FilteredTodosLoaded
                ? state.activeFilter
                : VisibilityFilter.all,
            activeStyle: activeStyle,
            defaultStyle: defaultStyle,
          );
          return AnimatedOpacity(
            opacity: visible ? 1.0 : 0.0,
            duration: Duration(milliseconds: 150),
            child: visible ? button : IgnorePointer(child: button),
          );
        });
  }
}

class _Button extends StatelessWidget {
  const _Button({
    Key key,
    @required this.onSelected,
    @required this.activeFilter,
    @required this.activeStyle,
    @required this.defaultStyle,
  }) : super(key: key);

  final PopupMenuItemSelected<VisibilityFilter> onSelected;
  final VisibilityFilter activeFilter;
  final TextStyle activeStyle;
  final TextStyle defaultStyle;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<VisibilityFilter>(
      key: ArchSampleKeys.filterButton,
      tooltip: ArchSampleLocalizations.of(context).filterTodos,
      onSelected: onSelected,
      itemBuilder: (BuildContext context) => <PopupMenuItem<VisibilityFilter>>[
            PopupMenuItem<VisibilityFilter>(
              key: ArchSampleKeys.allFilter,
              value: VisibilityFilter.all,
              child: Text(
                ArchSampleLocalizations.of(context).showAll,
                style: activeFilter == VisibilityFilter.all
                    ? activeStyle
                    : defaultStyle,
              ),
            ),
            PopupMenuItem<VisibilityFilter>(
              key: ArchSampleKeys.activeFilter,
              value: VisibilityFilter.active,
              child: Text(
                ArchSampleLocalizations.of(context).showActive,
                style: activeFilter == VisibilityFilter.active
                    ? activeStyle
                    : defaultStyle,
              ),
            ),
            PopupMenuItem<VisibilityFilter>(
              key: ArchSampleKeys.completedFilter,
              value: VisibilityFilter.completed,
              child: Text(
                ArchSampleLocalizations.of(context).showCompleted,
                style: activeFilter == VisibilityFilter.completed
                    ? activeStyle
                    : defaultStyle,
              ),
            ),
          ],
      icon: Icon(Icons.filter_list),
    );
  }
}

The FilterButton needs to respond to state changes in the FilteredTodosBloc so it uses BlocProvider to access the FilteredTodosBloc from the BuildContext. It then uses BlocBuilder to re-render whenever the FilteredTodosBloc changes state.

The rest of the implementation is pure Flutter and there isn’t much going on so we can move on to the ExtraActions widget.

Extra Actions

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Since this widget doesn’t care about the filters it will interact with the TodosBloc instead of the FilteredTodosBloc.

Let’s create widgets/extra_actions.dart and implement it.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class ExtraActions extends StatelessWidget {
  ExtraActions({Key key}) : super(key: ArchSampleKeys.extraActionsButton);

  @override
  Widget build(BuildContext context) {
    final todosBloc = BlocProvider.of<TodosBloc>(context);
    return BlocBuilder(
      bloc: todosBloc,
      builder: (BuildContext context, TodosState state) {
        if (state is TodosLoaded) {
          bool allComplete = (todosBloc.currentState as TodosLoaded)
              .todos
              .every((todo) => todo.complete);
          return PopupMenuButton<ExtraAction>(
            key: FlutterTodosKeys.extraActionsPopupMenuButton,
            onSelected: (action) {
              switch (action) {
                case ExtraAction.clearCompleted:
                  todosBloc.dispatch(ClearCompleted());
                  break;
                case ExtraAction.toggleAllComplete:
                  todosBloc.dispatch(ToggleAll());
                  break;
              }
            },
            itemBuilder: (BuildContext context) => <PopupMenuItem<ExtraAction>>[
                  PopupMenuItem<ExtraAction>(
                    key: ArchSampleKeys.toggleAll,
                    value: ExtraAction.toggleAllComplete,
                    child: Text(
                      allComplete
                          ? ArchSampleLocalizations.of(context)
                              .markAllIncomplete
                          : ArchSampleLocalizations.of(context).markAllComplete,
                    ),
                  ),
                  PopupMenuItem<ExtraAction>(
                    key: ArchSampleKeys.clearCompleted,
                    value: ExtraAction.clearCompleted,
                    child: Text(
                      ArchSampleLocalizations.of(context).clearCompleted,
                    ),
                  ),
                ],
          );
        }
        return Container(key: FlutterTodosKeys.extraActionsEmptyContainer);
      },
    );
  }
}

Just like with the FilterButton, we use BlocProvider to access the TodosBloc from the BuildContext and BlocBuilder to respond to state changes in the TodosBloc.

Based on the action selected, the widget dispatches an event to the TodosBloc to either ToggleAll todos’ completion states or ClearCompleted todos.

Next we’ll take a look at the TabSelector widget.

Tab Selector

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create widgets/tab_selector.dart and implement it.

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class TabSelector extends StatelessWidget {
  final AppTab activeTab;
  final Function(AppTab) onTabSelected;

  TabSelector({
    Key key,
    @required this.activeTab,
    @required this.onTabSelected,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      key: ArchSampleKeys.tabs,
      currentIndex: AppTab.values.indexOf(activeTab),
      onTap: (index) => onTabSelected(AppTab.values[index]),
      items: AppTab.values.map((tab) {
        return BottomNavigationBarItem(
          icon: Icon(
            tab == AppTab.todos ? Icons.list : Icons.show_chart,
            key: tab == AppTab.todos
                ? ArchSampleKeys.todoTab
                : ArchSampleKeys.statsTab,
          ),
          title: Text(tab == AppTab.stats
              ? ArchSampleLocalizations.of(context).stats
              : ArchSampleLocalizations.of(context).todos),
        );
      }).toList(),
    );
  }
}

You can see that there is no dependency on blocs in this widget; it just calls onTabSelected when a tab is selected and also takes an activeTab as input so it knows which tab is currently selected.

Next, we’ll take a look at the FilteredTodos widget.

Filtered Todos

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/filtered_todos.dart and let’s implement it.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class FilteredTodos extends StatelessWidget {
  FilteredTodos({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todosBloc = BlocProvider.of<TodosBloc>(context);
    final filteredTodosBloc = BlocProvider.of<FilteredTodosBloc>(context);
    final localizations = ArchSampleLocalizations.of(context);

    return BlocBuilder(
      bloc: filteredTodosBloc,
      builder: (
        BuildContext context,
        FilteredTodosState state,
      ) {
        if (state is FilteredTodosLoading) {
          return LoadingIndicator(key: ArchSampleKeys.todosLoading);
        } else if (state is FilteredTodosLoaded) {
          final todos = state.filteredTodos;
          return ListView.builder(
            key: ArchSampleKeys.todoList,
            itemCount: todos.length,
            itemBuilder: (BuildContext context, int index) {
              final todo = todos[index];
              return TodoItem(
                todo: todo,
                onDismissed: (direction) {
                  todosBloc.dispatch(DeleteTodo(todo));
                  Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
                    key: ArchSampleKeys.snackbar,
                    todo: todo,
                    onUndo: () => todosBloc.dispatch(AddTodo(todo)),
                    localizations: localizations,
                  ));
                },
                onTap: () async {
                  final removedTodo = await Navigator.of(context).push(
                    MaterialPageRoute(builder: (_) {
                      return DetailsScreen(id: todo.id);
                    }),
                  );
                  if (removedTodo != null) {
                    Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
                      key: ArchSampleKeys.snackbar,
                      todo: todo,
                      onUndo: () => todosBloc.dispatch(AddTodo(todo)),
                      localizations: localizations,
                    ));
                  }
                },
                onCheckboxChanged: (_) {
                  todosBloc.dispatch(
                    UpdateTodo(todo.copyWith(complete: !todo.complete)),
                  );
                },
              );
            },
          );
        } else {
          return Container(key: FlutterTodosKeys.filteredTodosEmptyContainer);
        }
      },
    );
  }
}

Just like the previous widgets we’ve written, the FilteredTodos widget uses BlocProvider to access blocs (in this case both the FilteredTodosBloc and the TodosBloc are needed).

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

Todo Item

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/todo_item.dart and let’s build it.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class TodoItem extends StatelessWidget {
  final DismissDirectionCallback onDismissed;
  final GestureTapCallback onTap;
  final ValueChanged<bool> onCheckboxChanged;
  final Todo todo;

  TodoItem({
    Key key,
    @required this.onDismissed,
    @required this.onTap,
    @required this.onCheckboxChanged,
    @required this.todo,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: ArchSampleKeys.todoItem(todo.id),
      onDismissed: onDismissed,
      child: ListTile(
        onTap: onTap,
        leading: Checkbox(
          key: ArchSampleKeys.todoItemCheckbox(todo.id),
          value: todo.complete,
          onChanged: onCheckboxChanged,
        ),
        title: Hero(
          tag: '${todo.id}__heroTag',
          child: Container(
            width: MediaQuery.of(context).size.width,
            child: Text(
              todo.task,
              key: ArchSampleKeys.todoItemTask(todo.id),
              style: Theme.of(context).textTheme.title,
            ),
          ),
        ),
        subtitle: todo.note.isNotEmpty
            ? Text(
                todo.note,
                key: ArchSampleKeys.todoItemNote(todo.id),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: Theme.of(context).textTheme.subhead,
              )
            : null,
      ),
    );
  }
}

Again, notice that the TodoItem has no bloc-specific code in it. It simply renders based on the todo we pass via the constructor and calls the injected callback functions whenever the user interacts with the todo.

Next up, we’ll create the DeleteTodoSnackBar.

Delete Todo SnackBar

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/delete_todo_snack_bar.dart and let’s implement it.

import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class DeleteTodoSnackBar extends SnackBar {
  final ArchSampleLocalizations localizations;

  DeleteTodoSnackBar({
    Key key,
    @required Todo todo,
    @required VoidCallback onUndo,
    @required this.localizations,
  }) : super(
          key: key,
          content: Text(
            localizations.todoDeleted(todo.task),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
          duration: Duration(seconds: 2),
          action: SnackBarAction(
            label: localizations.undo,
            onPressed: onUndo,
          ),
        );
}

By now, you’re probably noticing a pattern: this widget also has no bloc-specific code. It simply takes in a todo in order to render the task and calls a callback function called onUndo if a user presses the undo button.

We’re almost done; just two more widgets to go!

Loading Indicator

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Create widgets/loading_indicator.dart and let’s write it.

import 'package:flutter/material.dart';

class LoadingIndicator extends StatelessWidget {
  LoadingIndicator({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: CircularProgressIndicator(),
    );
  }
}

Not much to discuss here; we’re just using a CircularProgressIndicator wrapped in a Center widget (again no bloc-specific code).

Lastly, we need to build our Stats widget.

Stats

Our TodosBloc will be responsible for converting TodosEvents into TodosStates and will manage the list of todos in our application.
Let’s create widgets/stats.dart and take a look at the implementation.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/stats/stats.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class Stats extends StatelessWidget {
  Stats({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final StatsBloc statsBloc = BlocProvider.of<StatsBloc>(context);
    return BlocBuilder(
      bloc: statsBloc,
      builder: (BuildContext context, StatsState state) {
        if (state is StatsLoading) {
          return LoadingIndicator(key: FlutterTodosKeys.statsLoadingIndicator);
        } else if (state is StatsLoaded) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Padding(
                  padding: EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    ArchSampleLocalizations.of(context).completedTodos,
                    style: Theme.of(context).textTheme.title,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(bottom: 24.0),
                  child: Text(
                    '${state.numCompleted}',
                    key: ArchSampleKeys.statsNumCompleted,
                    style: Theme.of(context).textTheme.subhead,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    ArchSampleLocalizations.of(context).activeTodos,
                    style: Theme.of(context).textTheme.title,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(bottom: 24.0),
                  child: Text(
                    "${state.numActive}",
                    key: ArchSampleKeys.statsNumActive,
                    style: Theme.of(context).textTheme.subhead,
                  ),
                )
              ],
            ),
          );
        } else {
          return Container(key: FlutterTodosKeys.emptyStatsContainer);
        }
      },
    );
  }
}

We’re accessing the StatsBloc using BlocProvider and using BlocBuilder to rebuild in response to state changes in the StatsBloc state.

Putting it all together

Let’s create main.dart and our TodosApp widget. We need to create a main function and run our TodosApp.

void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();
  runApp(TodosApp());
}

Note*: We are setting our BlocSupervisor’s delegate to the* _SimpleBlocDelegate_ we created earlier so that we can hook into all transitions and errors.

Next, let’s implement our TodosApp widget.

class TodosApp extends StatelessWidget {
  final todosBloc = TodosBloc(
    todosRepository: const TodosRepositoryFlutter(
      fileStorage: const FileStorage(
        '__flutter_bloc_app__',
        getApplicationDocumentsDirectory,
      ),
    ),
  );

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: todosBloc,
      child: MaterialApp(
        title: FlutterBlocLocalizations().appTitle,
        theme: ArchSampleTheme.theme,
        localizationsDelegates: [
          ArchSampleLocalizationsDelegate(),
          FlutterBlocLocalizationsDelegate(),
        ],
        routes: {
          ArchSampleRoutes.home: (context) {
            return HomeScreen(
              onInit: () => todosBloc.dispatch(LoadTodos()),
            );
          },
          ArchSampleRoutes.addTodo: (context) {
            return AddEditScreen(
              key: ArchSampleKeys.addTodoScreen,
              onSave: (task, note) {
                todosBloc.dispatch(
                  AddTodo(Todo(task, note: note)),
                );
              },
              isEditing: false,
            );
          },
        },
      ),
    );
  }
}

Our TodosApp is a stateless widget which creates a TodosBloc and makes it available through the entire application by using the BlocProvider widget from flutter_bloc.

The TodosApp has two routes:

  • TodosLoading - the state while our application is fetching todos from the repository.
  • TodosLoaded - the state of our application after the todos have successfully been loaded.
  • TodosNotLoaded - the state of our application if the todos were not successfully loaded.

The entire main.dart should look like this:

import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/screens/screens.dart';

void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();
  runApp(TodosApp());
}

class TodosApp extends StatelessWidget {
  final todosBloc = TodosBloc(
    todosRepository: const TodosRepositoryFlutter(
      fileStorage: const FileStorage(
        '__flutter_bloc_app__',
        getApplicationDocumentsDirectory,
      ),
    ),
  );

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: todosBloc,
      child: MaterialApp(
        title: FlutterBlocLocalizations().appTitle,
        theme: ArchSampleTheme.theme,
        localizationsDelegates: [
          ArchSampleLocalizationsDelegate(),
          FlutterBlocLocalizationsDelegate(),
        ],
        routes: {
          ArchSampleRoutes.home: (context) {
            return HomeScreen(
              onInit: () => todosBloc.dispatch(LoadTodos()),
            );
          },
          ArchSampleRoutes.addTodo: (context) {
            return AddEditScreen(
              key: ArchSampleKeys.addTodoScreen,
              onSave: (task, note) {
                todosBloc.dispatch(
                  AddTodo(Todo(task, note: note)),
                );
              },
              isEditing: false,
            );
          },
        },
      ),
    );
  }
}

That’s all there is to it! We’ve now implemented a todos app in flutter using the bloc and flutter_bloc packages and we’ve successfully separated our presentation layer from our business logic.

The full source for this example can be found here.

If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.

Paths in Flutter: A Visual Guide

Paths in Flutter: A Visual Guide

In this article, we will go through each function which in Path class and see how they behave.

In this article, we will go through each function which in Path class and see how they behave.

Flutter gives us a lot of standard views to use in our projects, but from time to time we need to create custom views. We will go through each function which in Path class and see how they behave.

One of the most common way to do this is, using paths.

But first, let’s quickly go over our base playground.

p.s. I wanted to pick Totoro as a the header because of my latest trip to Japan and also it is cute :) Also there is a path in the gif :)

import 'package:flutter/material.dart';

void main() => runApp(
      MaterialApp(
        home: PathExample(),
      ),
    );

class PathExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: PathPainter(),
    );
  }
}

class PathPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    // TODO: do operations here
    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

Basically what we have here is a StatelessWidgetwith CustomPaintas a child and it gets our CustomPainteras a painter. CustomPaint is a widget that provides us a canvas to be used by the CustomPainter to paint what we provided in the paint method.

For painting options, we created a Paint object and decided to draw everything stroke style with a width of 8 in color red.

Next, we have our Path in our code to be used for drawing. Path is basically a collection of drawn elements. These elements are drawn according to its starting point. The initial starting point for a Path is (0,0).

Lastly, we have our canvas . We use canvas for drawing our path on it with the paint that we created.

Now that we are done with the playground, let’s talk about the screen coordinate system in case it is not familiar to some people.

For the screen, the start point (0,0) is the screen’s top left corner. X coordinate direction is the horizontal axis and the right edge of the screen is the positive visible limit and its value is the width of the screen. Y coordinate direction is the vertical axis and bottom edge of the screen is the positive visible limit and its value is the height of the screen.

Now we are ready to start.

moveTo

moveTo method helps us to move the starting point of the sub-path to the point provided within the method.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    // Moves starting point to the center of the screen
    path.moveTo(size.width / 2, size.height / 2);
    canvas.drawPath(path, paint);
}

Moves starting point to center

lineTo

lineTo is the method to draw a line from the current point of the path to the point provided within the method.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    // Draws a line from left top corner to right bottom
    path.lineTo(size.width, size.height);
    canvas.drawPath(path, paint);
}

Draws a line from top left to bottom right

lineTo example

quadraticBezierTo

quadraticBezierTo method was for me the most complicated one to understand. It draws a Bezier Curves and as we learn from Mathematics, it does this with the control point provided.

Disclaimer: Since it’s a complicated concept, I wanted to keep this as visual as possible, you can see how bezier curves are calculated.

From the left center of the screen, we draw a bezier curve to the right center of the screen.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.moveTo(0, size.height / 2);
    path.quadraticBezierTo(size.width / 2, size.height, size.width, size.height / 2);
    canvas.drawPath(path, paint);
}

cubicTo

cubicTo method adds a Bezier curve in cubic style.

Unlike to quadraticBezierTo, we assign two different control points for . This way you can have the bezier calculations for two control points to have cool views like a wave.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;
    
    Path path = Path();
    path.cubicTo(size.width / 4, 3 * size.height / 4, 3 * size.width / 4, size.height / 4, size.width, size.height);
    canvas.drawPath(path, paint);
}

conicTo

conicTo is also basically acting like a quadraticBeizerTo wit the only difference being the weight variable. If the weight is bigger than 1, the drawn shape is a hyperbola. If the weight is 1 then drawn shape is parabola and if it’s smaller than 1, the drawn shape would be an ellipse.

One important thing to note is, as the weight increases, the curve is pulled more to control point.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.conicTo(size.width / 4, 3 * size.height / 4, size.width, size.height, 20);
    canvas.drawPath(path, paint);
}

arcTo

arcTo is accepting an oval then, gets a start angle and sweep angle as a radian. It starts drawing the oval from the start angle and adds the sweep angle to the start angle.

e.g. For drawing an arc starting from left middle edge to top edge of an oval, we will start from 0 which is the radian value for 0 and add 1.57 which is the radian value for 90.

The example below shows the arc of an oval drawn from the center of the screen.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    // Method to convert degree to radians
    num degToRad(num deg) => deg * (Math.pi / 180.0);

    Path path = Path();
    path.arcTo(Rect.fromLTWH(size.width / 2, size.height / 2, size.width / 4, size.height / 4), degToRad(0), degToRad(90), true);
    canvas.drawPath(path, paint);
}

addRect

addRect adds a rectangle as a new sub-path.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    // Adds a rectangle
    path.addRect(Rect.fromLTWH(size.width / 2, size.height / 2, size.width / 4, size.height / 4));
    canvas.drawPath(path, paint);
}

addOval

addOval adds an oval as a sub-path. From the example above, we will only change the method call.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    // Adds an oval
    path.addOval(Rect.fromLTWH(size.width / 2, size.height / 2, size.width / 4, size.height / 4));
    canvas.drawPath(path, paint);
}

addArc

addArcis acting as arcTo.

e.g. For drawing an arc starting from left middle edge to top edge of an oval, we will start from 3.14 which is the radian value for 180 and add 1.57 which is the radian value for 90.

For making things easier, I added a function to calculate the radian from degree.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    // Method to convert degree to radians
    num degToRad(num deg) => deg * (Math.pi / 180.0);

    Path path = Path();
    // Adds a quarter arc
    path.addArc(Rect.fromLTWH(0, 0, size.width, size.height), degToRad(180), degToRad(90));
    canvas.drawPath(path, paint);
}

addPolygon

addPolygon method draws polygon from sets of points. It gets a set of Offset values which will be the positions for the polygon. Lastly it accepts a boolean, true acts like path.close() and draws a straight line between the last and first point and false does nothing.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    // Adds a polygon from the starting point to quarter point of the screen and lastly
    // it will be in the bottom middle. Close method will draw a line between start and end.
    path.addPolygon([
      Offset.zero,
      Offset(size.width / 4, size.height / 4),
      Offset(size.width / 2, size.height)
    ], true);
    canvas.drawPath(path, paint);
}

close = false

close = true

addRRect

addRRect is a method to create a rounded cornered rectangle. We will use the rectangle above and corners with radius of 16.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.addRRect(
      RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2, size.height / 2, size.width / 4, size.height / 4), Radius.circular(16))
    );
    canvas.drawPath(path, paint);
}

addPath

addPath is the method to add one path to another one with an offset. We will add one path with line to the rounded corner rectangle above.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.addRRect(
        RRect.fromRectAndRadius(Rect.fromLTWH(size.width / 2, size.height / 2, size.width / 4, size.height / 4), Radius.circular(16))
    );
    Path secondPath = Path();
    secondPath.lineTo(size.width / 2, size.height / 2);
    path.addPath(secondPath, Offset(16, 16));
    canvas.drawPath(path, paint);
}

relativeLineTo

relativeLineTo is basically behaving like lineTo but only offsets the drawn shape to the current point. Width the same code lineTo would have the half of the length.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.moveTo(size.width / 4, size.height / 4);
    path.relativeLineTo(size.width / 2, size.height / 2);
    canvas.drawPath(path, paint);
}

relativeQuadraticBezierTo

relativeQuadraticBezierTo method acts like quadraticBezierTo method. Current point is calculated relative to the current position of the path.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.moveTo(size.width / 4, size.height / 4);
    path.relativeQuadraticBezierTo(size.width / 2, size.height, size.width, size.height / 2);
    canvas.drawPath(path, paint);
}

relativeConicTo

relativeConicTo behaves exactly like conicto method. It calculates the current point relative to the path’s current position.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.moveTo(size.width / 4, size.height / 4);
    path.relativeConicTo(size.width / 4, 3 * size.height / 4, size.width, size.height, 20);
    canvas.drawPath(path, paint);
}

relativeCubicTo

relativeCubicTo method behaves exactly like cubicTo. Only difference is, it’s current position will be calculated relative to the current position of the path.

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8.0;

    Path path = Path();
    path.moveTo(size.width / 4, size.height / 4);
    path.relativeCubicTo(size.width / 4, 3 * size.height / 4, 3 * size.width / 4, size.height / 4, size.width, size.height);
    canvas.drawPath(path, paint);
}

Conclusion

Drawing custom shapes with canvas are really important and it’s helpful for us to use our creativity in our application development. These operations can be used to create a cool background, graphics and so on.

So, go ahead and play around with it, if you have any questions either leave a comment below.

Thank you!

Learn More

Getting started with Flutter

Android Studio for beginners

Flutter Tutorial - Flight List UI Example In Flutter

Let’s Develop a Mobile App in Flutter

Mastering styled text in Flutter

A Design Pattern for Flutter

Weather App with “flutter_bloc”

An introduction to Dart and Flutter

Learn Flutter & Dart to Build iOS & Android Apps

Flutter & Dart - The Complete Flutter App Development Course

Dart and Flutter: The Complete Developer’s Guide

Flutter - Advanced Course

Let’s Develop a Mobile App in Flutter

Let’s Develop a Mobile App in Flutter

Flutter is a mobile framework for the development of multiplatform native apps, so from one code we will get an app that can run on Android as well as on iOS. It is a similar experiment to Xamarin from Microsoft a few years ago.

Flutter is a mobile framework for the development of multiplatform native apps, so from one code we will get an app that can run on Android as well as on iOS. It is a similar experiment to Xamarin from Microsoft a few years ago.

Let’s Develop a Mobile App in Flutter (1/3)

Flutter developers released a second beta version of the Flutter framework at the beginning of April. It is the second beta version released in just the last month and a half. It seems that after a long development period, there will be a stable version of the framework released quite soon.

Flutter developers released a second beta version of the Flutter framework at the beginning of April. It is the second beta version released in just the last month and a half. It seems that after a long development period, there will be a stable version of the framework released quite soon. In this article we will present the basic interface setting for using Flutter and describe its project structure. We will also use a simple project from the Flutter developers to show how to create an app for Android and iOS that has a UI corresponding to the given platform.

If you are a Flutter guru already (or if you know your way around it), you will not find much new information in this article. But we believe that you will discover some interesting tips and tricks in this series nonetheless.

Let’s get started

The Dart programming language is used for creating Flutter applications. Dart is an object oriented programming language and its syntax is very similar to Kotlin’s, even though it might be a bit more wordy. You can check its specifications and syntax on the official website. We will not discuss Dart much in this article but we will still present a few basic language constructs.

SDKs

We need to have the Flutter and Dart SDKs installed in order to be able to start using Flutter. The installation of the SDK packages is partially dependent on your operating system. We will show only the main commands here, other details and differences can be found on the official Flutter webpage. Because Flutter is in beta, the SDK installation is performed by cloning the official repository, therefore you need to have a git client installed.

git clone -b beta https://github.com/flutter/flutter.git

Then you need to add the path to Flutter to the interface variable PATH. After a correct setup we can call Flutter relatively, thanks to which we ensure the correct function of all other available Flutter tools. Next, we will need the SDK and the relevant development tools for our intended platform. The only development restriction for iOS (or let’s say of the final app assembly) is the necessity of having a device with macOS. Finally, we need to install all the dependent components using the following command. The same command can also be used for the overall configuration check of our interface.

flutter doctor

As Flutter is constantly being developed, the stable SDK version changes on a daily basis. That is why we recommend running the following command every time before you start or continue the development of your Flutter app. It will perform the update of all necessary SDK packages.

flutter upgrade

Editor

We recommend using Android Studio for the development. Flutter developers offer a plugin that will make the creation of your new project a piece of cake. All the following examples will be presented in Android Studio. You can find all the necessary details for the installation of Android Studio and the plugin on the official Flutter website.

Project creation

If you have the Android Studio and the plugin installed, the creation of a new project is a cakewalk. Select the Create New Flutter Project option and go through a simple wizard that will help you set up the basic project configuration.

After the creation, the project contains all the necessary parts for a correct setup and run of the final app as well as a demo project prepared by Flutter developers. In the next part we will use this demo project to show how to create apps for both platforms with ease. In the end, the apps will look as if they are created by a native development process.

Project’s structure

The structure of every Flutter project looks more or less like this:

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.
dependencies:
  ...
  device_info: "^0.2.0"

Flutter application

Assembly of the app

To assemble the final app, we only need to enter one of the following commands, depending on the selected platform. The assembly can be parameterized to a certain degree. The currently supported parameters can be displayed by the last two commands.

flutter build apk          # Build an Android APK file from your app.
flutter build ios          # Build an iOS application bundle (Mac OS X host only).
flutter build apk --help   # Print build parameters
flutter build ios --help   # Print build parameters

As you can see, the final apps created from one code for iOS and Android look alike. After installing the demo app on a specific end device (iPhone X, Pixel 2 XL), it looks like this:

Expanding the demo app

Let’s add more simple logic to the app. After every fifth click on the + button, the app will show a simple dialogue with a query if the user wishes to reset the counter or continue. The user will then, using the dialogue buttons, choose one option. When the OK button is pressed, the counter will reset and the dialogue ends. When pressing the Cancel button, the dialogue will end. At the end of the _incrementCounter method from the _MyHomePageState class, we will add the following code:

if (_counter % 5 == 0) {
  showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return /*TODO create dialog*/
      });
}

Every time the variable _counter is divisible by five, the dialogue will be shown, so everything is set up as we wanted. Now we need to create a function that will create a dialogue with the requested functionality.

Material design dialogue

Let’s add a new method that will create a new alert dialogue in material design to the _MyHomePageState class. The basis is the AlertDialog class, which awaits in the constructor for the configuration of the dialogue being created. An important parameter is the actions field where the individual dialogue buttons are defined. The method for creating the dialogue is as follows:

AlertDialog _createMaterialAlertDialog() => new AlertDialog(
    title: new Text('Reset counter'),
    content: new Text('Do you want to reset counter?'),
    actions: <Widget>[
      new MaterialButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: new Text('Cancel'),
      ),
      new MaterialButton(
          onPressed: () {
            setState(() {
              _counter = 0;
            });
            Navigator.pop(context);
          },
          child: new Text('OK')),
    ],
  );

iOS design dialogue

The dialogue creation is the same as the preceding one, but instead of the material design component we will use components from the cupertino package. The method for creating the dialogue is as follows:

CupertinoAlertDialog _createCupertinoAlertDialog() => new CupertinoAlertDialog(
    title: new Text('Reset counter'),
    content: new Text('Do you want to reset counter?'),
    actions: <Widget>[
      new CupertinoButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: new Text('Cancel'),
      ),
      new CupertinoButton(
          onPressed: () {
            setState(() {
              _counter = 0;
            });
            Navigator.pop(context);
          },
          child: new Text('OK')),
    ],
  );

Dialogue based on the target platform

Let’s now combine both concepts together and add a logic that will ensure that the correct dialogue is displayed based on the platform on which the app is running. It is quite simple; we only need to ask what the used platform is.

if (_counter % 5 == 0) {
  showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return Platform.isIOS
            ? _createCupertinoAlertDialog()
            : _createMaterialAlertDialog();
      });
}

As you have probably noticed, the app now contains a lot of redundant code – the source code for both methods for the creation of corresponding dialogues is virtually the same. It differs only in places where we are calling the appropriate class for displaying a specific element. In one of the following articles we will show how to, at least partially, get rid of these redundancies.

Conclusion

In today’s article we did an overview of Flutter, showed the initial steps needed before starting the creation of a Flutter app and described the parts of the Flutter project. We also extended the prepared demo project with the platform dependent dialogue displaying functionality. You can find the whole project at our GitHub.

We will present the basic constructions of the Dart programming language next. We will also show how Flutter actually works from the inside and start creating an app for displaying events, event registrations, and notifications about any event changes.

Let’s Develop a Mobile App in Flutter (2/3)

Language Dart you need to know if you want a Flutter flow. That’s why we’ll have a look at how to write and design in Flutter. We’ll answer the basic question: Why does it work cross-platform? And we’ll also call a native service for the given platform.

The last article presented the Flutter framework: We’ve set up the environment in order to be able to start developing the Flutter application and described the structure of the Flutter app project. We also presented how to create the platform-dependent dialogue.

Today, we’ll introduce Flutter in more technical details. We’ll go over the Dart language in order to understand how to actually write in Flutter. We’ll talk about Flutter from a design perspective and explain why it works cross-platform. And we’ll show how to call a native service for the given platform. Let’s get started.

Language class

Dart is an object-oriented programming language with syntax very similar to today’s modern languages such as Kotlin, C# or Java.

Variables

Dart is a strongly typed language. That means that every variable has its own data type that is determined when the variable is created and it cannot be changed when the program is running. You don’t have to explicitly state the data type when creating a new variable. You can use the keyword var to deduce its type. Even though it’s a strongly typed language, you can create a variable “without any data type” by using the keyword dynamic.

// explicit data type
String firstName = 'Eman';

// data type deduction
var surname = 'Novák';

// variable data type
dynamic company = 'eMan.cz';

//firstName = 10; // compile error
//surname = 10; // compile error
company = 10;

Classes and objects

Everything is an object in Dart. Every object is an instance of a class and all classes inherit from the Object class. The classes consist of class variables and methods (static methods); objects consist of instance variables and methods.

One of the concepts of object-oriented programming is the encapsulation of classes. In Java, for example, you can define several visibility levels, usually by using keywords such as private, protected, or public. Only the private and public visibility, that which is not defined by special keywords, exist in Dart. But while declaring a class, variable etc. for private visibility, it’s sufficient to use the _ prefix in the name of the element. The visibility does not apply to the classes’ level but to the level of the individual app parts – libraries (library).

class _MyPrivateClass {
  String _myPrivateVariable = '...' // private variable in private class
  String myPublicVariable = '...' // public variable in private class
}

class MyPublicClass {
  String _myPrivateVariable = '...' // private variable in public class
  String myPublicVariable = '...' // public variable in public class
}

Note: Each individual Dart file is a library by default. That means that the individual private components are not visible among the files.

An interesting concept of this language is the Named constructors. It’s a classic object constructor but you can define its name. If the constructor is named appropriately, you can deduce how the object is created from its name. In Java, for example, a similar construct could be created using a static method that would create and initialize the actual object.

class Person {
  String firstName;
  String surname;

  Person.fromJson(Map data) {
    firstName = data['first_name'];
    surname = data['surname'];
  }
}

main() {
  var person = new Person.fromJson({'first_name': 'Eman', 'surname': 'von Prag'});
  print(person.firstName); // Eman
  print(person.surname);   // von Prag
}

Functions

Dart also supports classical functions and because everything is an object, so are the functions. The main function is the main() function that is the app’s entry function, therefore every app will start with this function. You can nest the definitions of the individual functions and create, so called, Nested functions. Every nested function gains the context of the function in which it’s nested, it can see its local variables.

bool topLevel = true;

void main() {
  var insideMain = true;

  void nestedFunction() {
    var insideNestedFunction = true;

    void nestednestedFunction() {
      var insideNestedNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideNestedFunction);
      assert(insideNestedNestedFunction);
    }

    //assert(insideNestedNestedFunction) translation error - unknown variable
  }
}

Null-aware operators

Similarly to Kotlin, Dart offers several operators that make notation of the Null Pointer safety code easier.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

Cascade

The cascade is for calling multiple methods on a single object. In Kotlin, a similar result is obtained by using the apply.

class Person {
  String firstName;
  String surname;
  String company;
}

main() {  
  var person = new Person()
    ..firstName = 'Bob'
    ..surname = 'Clever'
    ..company = 'eMan.cz';
}

Everything is a widget

You’ve probably heard that “everything is a widget” if you’ve read any article about Flutter. And yes, everything is actually a widget. Everything, even the app itself, is a widget. Thanks to this, every UI element can be addressed uniformly. There are two kinds of widgets:

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

Source: https://flutter.io/technical-overview/

Every widget is very small because it manages only the activity it was created for and not any other. For example, the Text widget that displays text manages only the actual display of the text and doesn’t consider its position in connection to the other widgets nor has the ability to gather events (e.g. clicks). If you want it to have any other function, you simply add another widget that supports the function you need. In the end, developing a Flutter app results in putting the individual widgets together in order to get the desired functionality, which reminds us of building with Legos. And everyone loves Legos, right?! 😉

Flutter technology

Flutter does not use any native elements of the target platform. All elements were developed anew by Flutter developers in order to be suitable to the conventions of the target platform. Most of the app’s source code is translated directly into machine code of the target processor (via Android NDK on Android and via LLVM on iOS). That’s why you only need one source code for both platforms. And thanks to this the resulting app performance should be way faster, at least on Android.

You can see the individual framework layers on the diagram below. The dependency of the individual layers is from the top down; each layer depends on the layers below. The green part shows the main components of the Flutter framework. As you can see, Material (Android) and Cupertino (iOS) components are on the same level, making the platform-dependent development quite complicated as we saw in the last article. When looking over the Flutter documentation on its website, you can find a diagram showing the Material component to be dependent on Cupertino. That would solve the above mentioned issue. Unfortunately, when looking into the current source code of Flutter, it seems that the two components are at the same level.

The blue part shows components present in the Flutter core that are responsible for the actual operation of the app. The Skia component is responsible for the rendering of 2D graphics, Dart for compilation of the AOT parts of the code that couldn’t be compiled into the machine code directly, and the Text component manages the rendering of the text.

Edited from source: https://flutter.io/technical-overview/

Native interface

Flutter allows for calling a specific method of the native interface of the given platform using the, so called, Method channel. The Flutter app (client) sends a message with the request to the native part of the app (host). If the host knows the message, it manages the request and returns an answer. For this to be correctly served, both the client and the host have to implement the same message protocol.

Source: https://flutter.io/platform-channels/

As you can see from the diagram, using Method channel you can call any service of the native platform or a service that is offered by third party libraries. It’s also one of the ways to implement a service using the platform-specific services that Flutter doesn’t support. This is useful when, for example, you don’t have a library with the requested service available for Flutter but it exists as a library for Android and iOS.

Let’s see this in action using an example scenario when you want to find out the battery status of your device. As Flutter supports the creation of the platform-specific parts of the source code even in Kotlin for Android parts, and in Swift for iOS parts, we used these languages for our examples.

Client

The client sends a request about the battery status and displays the formatted result in the middle of the screen upon its reception. The message processing is asynchronous so the state of the battery can be displayed only after the result is received. In the example you can see one of the possible methods of processing such asynchronous calls.

An important part is the construction of the MethodChannel object that awaits in the parameter of the constructor for the name of the channel that is being created. On the host side, you can connect the host and the client using the this name. In order for the client and host to communicate, they need to use the same channel as well as send and expect to receive messages of the same type. The invokeMethod() method is used for defining the message type (in the parameter of the method).

The main.dart file in the lib directory should then look like this:

const platform = const MethodChannel('battery');

void main() {
  platform
      .invokeMethod('getBatteryLevel') // sends message with request 'getBatteryLevel'
      .then((result) => 'Battery level at $result %.')
      .catchError((error) => "Failed to get battery level: '${error.message}'.")
      .then((msg) => runApp(new Center(
            child: new Text(
              msg,
              textDirection: TextDirection.ltr,
            ),
          )));
}

Host

It is important that the host registers the handler on the same channel that was defined on the client’s side. After receiving a specific message type, that can be determined by call.method, you can perform the requested operation and save the result in the result object.

The MainActivy.kt file in the android/app/src/main/kotlin/package_name directory should then look like this:

class MainActivity : FlutterActivity() {
    private val CHANNEL = "battery"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)

        MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "getBatteryLevel") {
                val batteryLevel = getBatteryLevel()

                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }

    private fun getBatteryLevel(): Int {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }
    }
}

The AppDelegate.swift file in the ios/Runner directory should then look like this:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        GeneratedPluginRegistrant.register(with: self);

        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController;
        let batteryChannel = FlutterMethodChannel.init(name: "battery", binaryMessenger: controller);
        batteryChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in
            if (call.method == "getBatteryLevel") {
                let batteryLevel : Int = self.getBatteryLevel();
                if(batteryLevel != -1) {
                    result(batteryLevel);
                } else {
                    result(FlutterError.init(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil));
                }
            } else {
                result(FlutterMethodNotImplemented);
            }
        });

        return super.application(application, didFinishLaunchingWithOptions: launchOptions);
    }

    private func getBatteryLevel() -> Int {
        let device = UIDevice.current;
        device.isBatteryMonitoringEnabled = true;
        if (device.batteryState == UIDeviceBatteryState.unknown) {
            return -1;
        } else {
            return Int(device.batteryLevel * 100);
        }
    }
}

Conclusion

Today we discussed what to use and how to write when designing apps in Flutter. We presented the individual parts of the app and the individual parts of the Flutter framework. And we also tackled the communication with the native platform interface when calling platform-specific services. You can find the whole project at eMan’s GitHub.

Let’s Develop a Mobile App in Flutter (3/3)

We already know how to create dialogues and use Dart. In the last article of this series, we will finally get to the creation of a more complex app. We’ll also discuss how to tackle the support of a platform dependent app. Let’s do this!

In the previous article we had a look into the Flutter framework, introduced Dart, and showed how to call the native interface of the given platform. This article will present possible localization options of the app and we’ll also have a brief look at the creation of a screen layout. Then we’ll incorporate all gained knowledge into our app and, as the final step, we’ll connect it to Firebase. That’ll give us a skeleton of what’s today probably the most used software architecture in mobile development – a mobile app that, in the client’s role, gathers data from a server database.

We make widgets (Abstract Factory pattern)

The first article tackled the creation of a platform dependent dialogue. The presented solution used the corresponding graphic component based on the identification of the target platform. This solution is not ideal as it calls a code with a large number of if-then-else constructions for the identification of the platform and subsequent usage of the specific component. This issue can be fixed, for example, by using the Abstract Factory design pattern.

Abstract Factory in general

The Abstract Factory design pattern is a part of a creational design pattern group. Abstract Factory is used for the creation of similar products and eliminates the usage of new. A group of objects created by a factory is then clearly determined by the relevant factory.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

(Source: https://en.wikipedia.org/wiki/Abstract_factory_pattern)

To better understand it, picture this example: You’re building a house and want the windows and doors to be from a specific material. You can select from wood, plastic and aluminium, and the only restriction is that you can’t combine various materials.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

You make your order in a given factory and you know that the supplied products will be from the same material. This way you get rid of asking over and over again: “What material should the door/window be made of?” Instead, you just submit another order to the factory you selected at the beginning.

Abstract Factory in more detail

Our abstract factory will offer an interface for the creation of various graphics and other elements. The creation of the elements itself will be taken care of by two factories, one for Android and another for iOS style. But how do you determine what elements to create in the factory and what elements to create in a different way? We can approach this question from several angles:

  1. creating all graphic components
  2. creating all components that are in the material or cupertino package
  3. creating only the components that exist in both graphic versions

We decided to choose the second option as it’s a nice compromise between the other possibilities. Moreover, if an alternative component is implemented in the other package in the future, adding the component will be way easier. Then the source files should not contain any package:flutter/cupertino.dart or ipackage:flutter/material.dart import in other files than the ones containing the implementation of relevant factories.

When declaring the individual interface methods, it’s recommended to use the “Optional named parameters“ thanks to which the interface change (in a sense of adding a new parameter) influences only the specific factories implementing the given interface and leaves the places where the interface methods are called untouched.

Here is the creation of Scaffold and AppBar components as an example. You can find the whole interface in the widget_factory.dart file.

abstract class WidgetFactory {
  Widget createScaffold({PreferredSizeWidget appBar, Widget body});

  PreferredSizeWidget createAppBar({Widget title});
  ...
}

The implementation of the given factories that ensures the creation of the relevant component based on the requested style:

class _AndroidWidgetFactory implements WidgetFactory {
  @override
  Widget createScaffold({PreferredSizeWidget appBar, Widget body}) {
    return Scaffold(appBar: appBar, body: body);
  }

  @override
  PreferredSizeWidget createAppBar({Widget title}) {
    return AppBar(title: title);
  }
  ...
}

class _IOSWidgetFactory implements WidgetFactory {
  @override
  Widget createScaffold({PreferredSizeWidget appBar, Widget body}) {
    return CupertinoPageScaffold(navigationBar: appBar, child: body);
  }

  @override
  PreferredSizeWidget createAppBar({Widget title}) {
    return CupertinoNavigationBar(middle: title, backgroundColor: Colors.white);
  }
  ...
}

All the necessary classes are ready. We only need to add the logic for the initialization of the factory that will create the elements while the app is running. We used a Singleton design pattern that will make sure that the whole running app will use only one instance of the specific factory. Dart offers the “Factory constructor“ for the creation and a clean usage of Singletons. As we can see from the example, the logic for determining on what system the app is running is only in one place.

abstract class WidgetFactory {
  static WidgetFactory _instance;

  factory WidgetFactory() {
    if (_instance == null) {
      if (Platform.isAndroid) {
        _instance = _AndroidWidgetFactory();
      } else if (Platform.isIOS) {
        _instance = _IOSWidgetFactory();
      } else {
        throw UnsupportedError('Unsupported target platform.');
      }
    }
    return _instance;
  }
  ...
}

// using
WidgetFactory().createScaffold(...);

Note: We could use a static method getInstance() for the creation of the Factory construction for returning the class instance in Java.

Pros and cons

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.
Localization of the app

Today’s standard is that apps are offered in various language mutations in order to attract as wide a spectrum of users as possible. And Flutter is one of them, offering several options on how to get the required properties. Using the intl package for the support of localization is one of the possible options. The package adds the support for localization to any project written in Dart. It establishes a special construction for getting the string’s definitions in the project that ensures the usage of a correct language mutation. Based on this construction and using a special tool, files with corresponding strings are generated for each language mutation that supplement the translation. These are then added to the final app. The second option we’ll present is defining the translation directly in the source code.

We’ll add a new dependency to the pubspec.yaml file that will provide the support for localization.

dependencies:
  flutter_localizations:
    sdk: flutter
  ...

In order to add support for any language to our app, we need to add two classes, as shown in the example below. The _EventsLocalizationsDelegate class is for the loading and storing of the EventsLocalizations object with the relevant, supported localization. Both classes are quite usual and they’ll be similar in every project. The only thing that changes is the values defined in the _localizedValues attribute. It’s a master map where the keys of the first map are marks according to the IANA register.

enum StringId {
  appTitle,
  homeScreenTitle,
  ...
}

class EventsLocalizations {
  static const LocalizationsDelegate<EventsLocalizations> delegate = const _EventsLocalizationsDelegate();

  static const Map<String, Map<StringId, String>> _localizedValues = {
    'en': {
      StringId.appTitle: 'Eman\'s events',
      StringId.homeScreenTitle: 'Events',
      ...
    },
    'cs': {
      StringId.appTitle: 'Eman\'s events',
      StringId.homeScreenTitle: 'Akce',
      ...
    },
    ...
  };

  static final Iterable<Locale> supportedLocales = _localizedValues.keys.map((languageCode) => Locale(languageCode));

  static EventsLocalizations of(BuildContext context) => Localizations.of<EventsLocalizations>(context, EventsLocalizations);

  final Locale _locale;

  EventsLocalizations(this._locale);

  String get appTitle => _localizedValues[_locale.languageCode][StringId.appTitle];
  String get homeScreenTitle => _localizedValues[_locale.languageCode][StringId.homeScreenTitle];
  // or
  String getString(StringId stringId) => _localizedValues[_locale.languageCode][stringId];
}

class _EventsLocalizationsDelegate extends LocalizationsDelegate<EventsLocalizations> {
  const _EventsLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => EventsLocalizations._localizedValues.containsKey(locale.languageCode);

  @override
  Future<EventsLocalizations> load(Locale locale) => SynchronousFuture<EventsLocalizations>(EventsLocalizations(locale));

  @override
  bool shouldReload(LocalizationsDelegate<EventsLocalizations> old) => false;
}

The last thing to do is to specify which language mutations are supported by the app (the supportedLocales parameter of the app’s widget) and register the delegates for all components that’ll be responsible for the correct localization (the localizationsDelegates field of the app’s widget).

class EventsApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: [
        EventsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: EventsLocalizations.supportedLocales,
      ...
    );
  }
}

Then to get a localized string we’ll use one of the following methods based on what localization class interface we selected.

EventsLocalizations.of(context).appTitle;
// or
EventsLocalizations.of(context).getString(StringId.appTitle);

Note: The current Material Components version does not support Czech, it introduces an error while rendering widgets.

Creating layouts

As previously stated, the layout creation is simply a puzzle. You just put together simple widgets, thus creating a widget that looks and does what was expected of it from the beginning. The following example shows the creation of ListView and its elements and we’ll also take a look at the usage of a few basic layouts. If you’d like to create more complex layouts, take a look at the Flutter webpage for some ideas to get you started.

class EventListState extends State<EventList> {
  final _events = List<Event>();

  @override
  void initState() {
    super.initState();
    _events.addAll(generateWordPairs().take(10).map((pair) => Event(
          pair.asPascalCase,
          DateTime.now(),
          lorem.createParagraph(),
        )));
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        padding: const EdgeInsets.all(20.0),
        itemCount: _events.length * 2,
        itemBuilder: (context, index) {
          if (index.isOdd) {
            return WidgetFactory().createDivider();
          } else {
            return _buildItem(_events[index ~/ 2]);
          }
        });
  }

  Widget _buildItem(Event event) {
    return GestureDetector(
      child: Container(
        decoration: BoxDecoration(color: Colors.transparent),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              padding: const EdgeInsets.only(bottom: 8.0),
              child: Text(
                event.name,
                style: TextStyle(fontSize: 20.0, color: Colors.black),
              ),
            ),
            Text(
              format.format(event.dateTime),
              style: TextStyle(
                fontSize: 16.0,
                color: Colors.grey[500],
              ),
            ),
          ],
        ),
      ),
      onTap: () => _itemClicked(event),
    );
  }
  ...
}

We’ll focus only on the _buildItem method that creates the layout of a single item of the list.

  • The android directory – here we will find the classic Android project. In the app module we can find the actual Android app that already contains a few generated files. These are for connecting the Flutter app to the classic Android app. All Android specific configurations, the applicationId definition for example, supported SDKs, or signing certificates will be modified here.
  • The ios directory – here we will find the typical iOS project. It already contains a few generated files used for connecting the Flutter app to the classic iOS app. All iOS specific configuration is to be done in this directory
  • The lib directory – the most important directory where the Flutter app files are stored. The main file here is main.dart containing the void main() function which is the input function for the Flutter app.
  • File pubspec.yaml – this file is for the Flutter app configuration. We also define the dependencies on external Flutter modules and Dart libraries in this file. For example, to add a library for detecting information about the device on which the app runs, we only need to add the following dependency.

You can picture the layout as a tree where every parent nod determines the behaviour or manner of the rendering of its children.

Cloud Firestore database

Cloud Firestore is a Document Store database; it’s the successor to the original Firebase real-time database. We’ll create a Firestore database for our example, with a collection named events containing several documents with the following structure:

name: "...",
date_time: "...",
description: "...",

We won’t go into details of the actual Cloud Firestore database creation and its addition to the project. For more details on this process, please have a look at this guide from Google. After the actual configuration, adding it to the project is a piece of cake. In the previous example, the initState method created a list of events containing random dates that we will now switch for dates from the database. We’ll get the events collection from an instance of the database and all documents that it contains will be mapped by the Event model class. In the following example we regard the events collection as a data stream and, using the listen method, we react to any change of the data. Feel free to try for yourself how the app reacts to you changing the data in the database.

class EventListState extends State<EventList> {
  ...
  @override
  void initState() {
    Firestore.instance
        .collection('events')
        .orderBy('date_time')
        .snapshots()
        .listen((event) => setState(() {
              _events.clear();
              _events.addAll(event.documents.map((snapshot) => Event(
                    snapshot.data['name'],
                    DateTime.parse('${snapshot.data['date_time']}z'),
                    snapshot.data['description'],
                  )));
            }));
  ...
}

Conclusion

In this article we presented how to tackle the issue of keeping the style for a given platform, and we created a support for a multi-language app. Then we created a simple layout and connected the app to the Firestore database. You can find the whole project that we have been very secretly creating here, one part at a time, at our GitHub, where you can also see that the project’s history follows this article.