UITableView Header section height is not adjusted automatically for a Custom UIView() class

I am having UIView() class where I am adding a label programatically and also given constraints to automatically adjust height of view based on content. I have used this class as HeaderView for a UItableView section. But the problem here is the height of this view is not adjusting accordingly to its content.

I am having UIView() class where I am adding a label programatically and also given constraints to automatically adjust height of view based on content. I have used this class as HeaderView for a UItableView section. But the problem here is the height of this view is not adjusting accordingly to its content.

Here he is my code of that custom View.

class DynamicHeaderView: UIView {

override func draw(_ rect: CGRect) {
let headerLabel = UILabel()
headerLabel.numberOfLines = 0
headerLabel.sizeToFit()
headerLabel.text = "This is header view. It is dynamicaaly growing text and will automaticaly get adjusted to it"
self.backgroundColor = .green
headerLabel.translatesAutoresizingMaskIntoConstraints = false

self.addSubview(headerLabel)

self.addConstraint(NSLayoutConstraint(item: headerLabel, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 16))

self.addConstraint(NSLayoutConstraint(item: headerLabel, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: -16))

self.addConstraint(NSLayoutConstraint(item: headerLabel, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 10))

} }

Code that I have written in my viewController,

 override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.countriesTable.sectionHeaderHeight = UITableView.automaticDimension;
self.countriesTable.estimatedSectionHeaderHeight = 25 }

func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView? {
let headerView = DynamicHeaderView()
return headerView
}

The height is always stick to the estimated header height as 25 which i have given in viewDidLoad() function.

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.

Best IDE and tools for Flutter development

Best IDE and tools for Flutter development

Flutter is an application development framework from Google for creating cross-platform mobile applications (in iOS and Android).

Things such as Hot Reload, a vast widget catalog, very good performance, and a solid community contribute to meeting that objective and makes Flutter a pretty good framework.

Imagine if you could make Flutter development faster, and see an instant boost in your workflow.

or even better:

What if there were best Flutter IDE and **tools **to choose from.

…and each of them could make your development faster and easier?

You’d probably be pumped to read about them.

Lucky for you, that’s exactly what I’m going to share with you in this post:

Best Flutter IDEs and tools can become an unparalleled ally when it comes to code completion and visual assistance for debugging and building a Flutter app.

These IDEs and tools are the best you can find for developing mobile applications. But to use these with Flutter, we will need to use a few plugins.

Flutter IDEs

For Flutter, it’s best to use Android Studio/IntelliJ or **Visual Studio **(VS) code with Mac/Windows as your operating systems.

These IDEs are the best you can find for developing mobile applications. But to use these with Flutter, we will need to use a few plugins.

We will need a plugin for the Dart compiler, another for code analysis, and another for the Flutter developer workflow (building, running, and debugging).

1. Android Studio

Android Studio or IntelliJ is the Recommend IDE by Flutter developers.

It provides you code completion, syntax highlighting, widget editing assists, run & debug support, and more.

There’s a browser-dev-tools style Inspector and more in the debug tools.

Download Android Studio

2. Visual Studio Code

Visual Studio Code is also recommended by Flutter Developers, This IDE built for developing and debugging web applications as well as Flutter apps. it has integrated Git control & terminal. It has a very large plugin system that probably rivals the Sublime quality and surpasses it for web language. It has all the necessary features and plugins to develop a full-fledged Flutter app with ease.

Download VsCode

Pub and Libraries

pub is the Dart package management system. There’s a website where can browse, search and get info about all the available packages for Dart and Flutter.

Dart Pub Packages

Gitter

If you run into any problems, you can ask for help in real time on the Flutter Gitter Channel. There you’ll find Flutter team members from Google as well as a supportive community of fans.

Frameworks

Standard

  • Bloc – Collection of packages that help implement the BLoC design pattern by Felix Angelov.
  • MobX – Supercharge the state-management in your apps with Transparent Functional Reactive Programming (TFRP). Port of MobX from the Js/React land.

Redux / ELM / Dependency Injection

  • Built redux – Automatic subscribing to your redux stores. Based on the built pattern by David Marne.
  • Redux.dart – Port of Redux to Dart with an ecosystem of middleware, Flutter integrations, and time traveling dev tools by John Ryan and Brian Egan.
  • Redux – Built to work with redux.dart, utilities that allow you to easily consume a Redux Store to build Widgets.
  • Dartea – Model View Update inspired by TEA from ELM by Shilyagov P.
  • Inject – Compile-time dependency injection by Google.
  • Flux – Implementation of the Flux framework by Google.
  • Fish – Alibaba Redux implementation.
Data Game Engine

How to build a Chat App with React Native

How to build a Chat App with React Native

In this tutorial, we’ll be looking at how to build a demo chat app in React Native

The following features:

  • Public and private chat rooms
  • Roles and permissions
  • Typing indicators
  • Read receipt
  • File uploads
  • Show online and offline users
Prerequisites

Knowledge of **React **and React Native is required to follow this tutorial.

The following package versions are used. If you encounter any issues in compiling the app, try to use the following:

  • Node 11.2
  • Yarn 1.13
  • React Native 0.59

You’ll also need a Chatkit app instance. I assume you already know how to set it up. If not, then check out the official docs.

Lastly, you’ll need an ngrok account for exposing the server to the internet.

Chat App overview

As mentioned earlier, we will be building a chat app. The first screen that the user will see when they open the app is the Login screen. This is where they enter their username so they can log in. We will be creating the users in the **Chatkit **console later:

Once logged in, the app will show the list of rooms they can enter:

Once they’ve selected a room to enter, they’ll be greeted by the Chat app screen:

From the Chat screen, when a user clicks on the avatar of another member of a room, they can see the last message they have seen:

The list of members can be viewed when they click on the Users button in the header. The circle right beside the username indicates their online status:

Aside from those, the app will also have a file attachment and typing indicator feature. Image, audio, and video files can also be previewed.

You can find the code in this GitHub repo.

Setting up roles and permissions

Before we proceed, we first have to set up the roles and permissions for each user in the Chatkit console. We will set up seven users:

Three rooms will also be needed:

  • company
  • web_dev
  • mobile_dev

Everyone will be able to have chat in the company room. While the web and mobile users can only chat within their respective rooms. This can be implemented by means of roles and permissions:

For this app, we only have two roles. These will only be scoped in a specific room:

  • room-admin - check all the permissions.
  • room-member - check all the permissions except room:delete, room:update, room:join, room:leave, room:members:add, room:members:remove.

The only difference between the two is that the room-admin can add or remove users from the room. Though we won’t really be implementing it in the app.

Once you’ve created the roles, the next step is to assign the roles to each of the users. Web Admin and Mobile Admin will have the role of room-admin, while the rest of the users will have the role of room-member. Be sure to apply them to the room where they’re supposed to go:

Don’t forget to assign all of the users as a member of the company room. This time, the room admins will have the same role as the members.

Bootstrapping the app

For us to get into the crucial parts of the app quickly, I prepared a GitHub repo which contains some of the boilerplate code (custom components, navigation setup, styling). Clone it and switch to the starter branch:

    git clone https://github.com/anchetaWern/RNChatkitDemo
    git checkout starter

Whenever I mention “starter branch“, it means that I’m referring to existing code.
Next, install all the dependencies and link the relevant packages:

    yarn
    react-native eject
    react-native link react-native-gesture-handler
    react-native link react-native-permissions
    react-native link react-native-document-picker
    react-native link react-native-fs
    react-native link react-native-config
    react-native link react-native-vector-icons
    react-native link rn-fetch-blob

One of the dependencies (React Native Audio Toolkit) doesn’t play nice with automatic linking. Be sure to check out their docs for information on how to set it up.

Next, we need to update the Android manifest file to allow the app to read from the external storage. This allows us to pick files from the device’s external storage:

    // android/app/src/main/AndroidManifest.xml
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.rnchatkitdemo">
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
      ...
    </manifest>

Lastly, update the android/app/build.gradle file and include the gradle file for React Native Config:

    apply from: "../../node_modules/react-native/react.gradle"
    apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" // add this

Adding the server code

Now we’re ready to start coding. Let’s kick things off with the server. Some of the code for initializing Chatkit and running the server itself has already been added to the starter branch. All we have to do now is install the dependencies, update the .env file, and add the routes that the app will be consuming.

Start by install the dependencies:

    cd server
    yarn

Next, update the .env file with the Chatkit credentials:

    CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID (v1:us1:)"
    CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"

Now we can proceed with the code. The /users route allows us to fill the users array with users data that will be used when someone logs in. Later on, be sure to access <a href="http://localhost:5000/users" target="_blank">http://localhost:5000/users</a> on your browser before you try logging in a user. This is because the login code uses the data in the users array to get specific user data:

    // server/index.js
    let users = [];
    app.get("/users", async (req, res) => {
      try {
        users = await chatkit.getUsers();
        res.send({ users });
      } catch (get_users_err) {
        console.log("error getting users: ", get_users_err);
      }
    });

Note: We only have the above route to simplify things. You don’t really want to be exposing user data in a production app.
When a user logs in, the app makes a request to the /user route. This will return the user’s info based on their username:

    app.post("/user", async (req, res) => {
      const { username } = req.body;
      try {
        const user = users.find((usr) => usr.name == username);
        res.send({ user });
      } catch (get_user_err) {
        console.log("error getting user: ", get_user_err);
      }
    });

Next, add the route which returns the rooms that the user (the one supplied in the request body) is a member of. This is the reason why we went through the trouble of assigning each of the users to their rooms:

    app.post("/rooms", async (req, res) => {
      const { user_id } = req.body;
      try {
        const rooms = await chatkit.getUserRooms({
          userId: user_id
        });
        res.send({ rooms });
      } catch (get_rooms_err) {
        console.log("error getting rooms: ", get_rooms_err);
      }
    });

The getUserRooms is preferable to the getJoinable rooms method because the rooms we created (except for the company room) are private. This means they won’t show up when you call getJoinableRooms. Another reason is that the room wouldn’t even be returned if the user is already a member of that room.
Lastly, add the route for returning the user permissions. This will be called when a user enters a specific room so that the app knows what the user can or cannot do in that specific room:

    app.post("/user/permissions", async(req, res) => {
      const { room_id, user_id } = req.body;
      try {
        const roles = await chatkit.getUserRoles({ userId: user_id });
        const role = roles.find(role => role.room_id == room_id);
        const permissions = (role) ? role.permissions : [];
        res.send({ permissions });
      } catch (user_permissions_err) {
        console.log("error getting user permissions: ", user_permissions_err);
      }
    });

The response data in the API call above is an array containing the individual permissions. Here’s an example: ['room:join', 'room:leave'].

Group screen

The Group screen is where the user will be redirected to when they have logged in to the app. We won’t go through the login code as it has already been pre-coded. All you need to know is that the Login screen sends a request to the /user route of the server. As you’ve seen earlier, this returns the user data. From this data, the only thing we want to pass to the Group screen is the user_id:

    // src/screens/Group.js
    state = {
      rooms: []
    };

    constructor(props) {
      super(props);
      const { navigation } = this.props;
      this.user_id = navigation.getParam("id");
    }

When the component is mounted, we send a request to the /rooms route. This returns the list of rooms that the user is a member of:

    async componentDidMount() {
      try {
        const response = await axios.post(`${CHAT_SERVER}/rooms`, { user_id: this.user_id });
        const { rooms } = response.data;
        this.setState({
          rooms
        });
      } catch (get_rooms_err) {
        console.log("error getting rooms: ", get_rooms_err);
      }
    }

Next, render the list of rooms:

    render() {
      const { rooms } = this.state;
      return (
        <View style={styles.container}>
          {
            rooms &&
            <FlatList
              keyExtractor={(item) => item.id.toString()}
              data={rooms}
              renderItem={this.renderRoom}
            />
          }
        </View>
      );
    }

Here’s the code for rendering each list item. It has a button which when clicked by the user redirects them to the room they selected:

    renderRoom = ({ item }) => {
      return (
        <View style={styles.list_item}>
          <Text style={styles.list_item_text}>{item.name}</Text>
          <Button title="Enter" color="#0064e1" onPress={() => {
            this.enterChat(item);
          }} />
        </View>
      );
    }

Here’s the code for entering a chat room. This sends a request to the /user/permissions route which then returns the permissions for that specific room. From there, we determine if the user is a room admin if they have the room:members:add permission. This means that they can add new users to the room:

    enterChat = async (room) => {
      try {
        const response = await axios.post(`${CHAT_SERVER}/user/permissions`, { room_id: room.id, user_id: this.user_id });
        const { permissions } = response.data;
        const is_room_admin = (permissions.indexOf('room:members:add') !== -1);

        this.props.navigation.navigate("Chat", {
          user_id: this.user_id,
          room_id: room.id,
          room_name: room.name,
          is_room_admin
        });

      } catch (get_permissions_err) {
        console.log("error getting permissions: ", get_permissions_err);
      }
    };

We won’t really be implementing the adding of users in this tutorial. This is just to show how permissions can be used to limit what the user can do within the app.##

Chat screen

We can now proceed to the main meat of the app. As we have lots of things to implement, I’ve divided it into multiple sections, each discussing a specific feature of the chat app.

Importing the dependencies

First, we import the dependencies:

    // src/screens/Chat.js
    import React, { Component } from "react";
    import { View, Text, ActivityIndicator, FlatList, TouchableOpacity, Alert } from "react-native";
    import { GiftedChat, Send, Message } from "react-native-gifted-chat"; // for the chat UI
    import { ChatManager, TokenProvider } from "@pusher/chatkit-client"; // for implementing chat functionality
    import axios from "axios"; // for making requests to the server
    import Config from "react-native-config"; // for reading .env file
    import Icon from "react-native-vector-icons/FontAwesome"; // for showing icons
    import { DocumentPicker, DocumentPickerUtil } from "react-native-document-picker"; // for picking files
    import * as mime from "react-native-mime-types"; // for determining the mime type of the picked file
    import Modal from "react-native-modal";
    import RNFetchBlob from "rn-fetch-blob"; // for converting the attached file to a blob
    import RNFS from "react-native-fs"; // for getting the base64 string representation of a file

Chatkit expects the file attachments to be a Blob. React Native doesn’t really have support for this format by default. So we use rn-fetch-blob as a polyfill to implement it. Later on, you’ll see this in action in the Attaching files section:

    const Blob = RNFetchBlob.polyfill.Blob;
    const fs = RNFetchBlob.fs;
    window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest;
    window.Blob = Blob;

Next, we have a few pre-coded components which are used to render either an audio or video player depending on the format of the attached file. Note that these components only support mp3 files for audio, and mp4 files for video:

    import ChatBubble from "../components/ChatBubble";
    import AudioPlayer from "../components/AudioPlayer";
    import VideoPlayer from "../components/VideoPlayer";

You can read my previous tutorial on playing audio and video file attachments in a Chatkit React Native app if you want to learn more about how those components are implemented
Lastly, update the .env file with your Chatkit config.

Initializing the chat code

In the Chat screen’s header, we add a button for opening the modal for viewing all the members of the room (whether they’re offline or online):

    class Chat extends Component {

      static navigationOptions = ({ navigation }) => {
        const { params } = navigation.state;
        return {
          headerTitle: params.room_name,
          headerRight: (
            <View style={styles.header_right}>
              <TouchableOpacity style={styles.header_button_container} onPress={params.showUsersModal}>
                <View style={styles.header_button}>
                  <Text style={styles.header_button_text}>Users</Text>
                </View>
              </TouchableOpacity>
            </View>

          ),
          headerStyle: {
            backgroundColor: "#333"
          },
          headerTitleStyle: {
            color: "#FFF"
          }
        };
      };

      // next: initialize state

    }

Next, initialize the state:

    state = {
      company_users: null,
      room_users: null,
      messages: [],
      is_initialized: false, // if Chatkit is initialized or not
      is_picking_file: false, // to show/hide the loading animation when picking files
      // to show/hide the various modal windows:
      is_video_modal_visible: false,
      is_last_viewed_message_modal_visible: false,
      is_users_modal_visible: false, 
      is_typing: false, // if there's someone in the room who is currently typing
      typing_user: null, // the username of the user who is typing
      show_load_earlier: false, 
      viewed_user: null, // username of user whose read receipt is currently being viewed
      viewed_message: null // the text message being viewed
    };

    // next: add constructor

Next, get the navigation params that were passed from the Group screen earlier:

    constructor(props) {
      super(props);
      const { navigation } = this.props;

      this.user_id = navigation.getParam("user_id");
      this.room_id = navigation.getParam("room_id");
      this.is_room_admin = navigation.getParam("is_room_admin");

      this.modal_types = {
        video: 'is_video_modal_visible',
        last_viewed_message: 'is_last_viewed_message_modal_visible',
        users: 'is_users_modal_visible'
      }
    }

    // next: add componentDidMount

Once the component is mounted, initialize Chatkit:

    async componentDidMount() {
      this.props.navigation.setParams({
        showUsersModal: this.showUsersModal
      });

      try {
        const chatManager = new ChatManager({
          instanceLocator: CHATKIT_INSTANCE_LOCATOR_ID,
          userId: this.user_id,
          tokenProvider: new TokenProvider({ url: CHATKIT_TOKEN_PROVIDER_ENDPOINT })
        });

        let currentUser = await chatManager.connect();
        this.currentUser = currentUser;

        // next: subscribe to room

      } catch (chat_mgr_err) {
        console.log("error with chat manager: ", chat_mgr_err);
      }
    } 

We then subscribe to the room. We only need to subscribe to the hooks for receiving messages, and showing/hiding typing indicators:

    await this.currentUser.subscribeToRoomMultipart({
      roomId: this.room_id,
      hooks: {
        onMessage: this.onReceive,
        onUserStartedTyping: this.startTyping,
        onUserStoppedTyping: this.stopTyping
      }
    });

We don’t need to subscribe to the onPresenceChanged hook because the presence data from this.currentUser.users is live data. This means that when the users either offline or online, their presence state also changes, so the UI is automatically updated.
Lastly, we update the state with the room members data:

    await this.setState({
      is_initialized: true,
      room_users: this.currentUser.users
    });

Chat UI

We can now proceed to the UI. Start by extracting the data that we need from the state:

    render() {
      const {
        is_initialized,
        room_users,
        messages,
        video_uri,
        is_video_modal_visible,
        is_last_viewed_message_modal_visible,
        viewed_user,
        viewed_message,
        is_users_modal_visible,
        show_load_earlier,
        typing_user
      } = this.state;

      // next: render the Chat UI
    }

Next, render the chat UI. All the heavy lifting is already done for us by React Native Gifted Chat, so all we have to do is supply all the props that it needs:

    return (
      <View style={styles.container}>
        {(!is_initialized) && (
          <ActivityIndicator
            size="small"
            color="#0064e1"
            style={styles.loader}
          />
        )}

        {is_initialized && (
          <GiftedChat
            messages={messages}
            onSend={messages => this.onSend(messages)} // function to execute when send button is clicked
            user={{
              _id: this.user_id
            }}
            renderActions={this.renderCustomActions} // for rendering button for attaching files
            renderSend={this.renderSend} // custom send button UI
            renderMessage={this.renderMessage} // custom chat bubble UI
            onInputTextChanged={this.onTyping} // function to execute while the user is typing
            renderFooter={this.renderFooter} // for rendering the typing indicator
            extraData={{ typing_user }} // so that the footer will be re-rendered when the typing user is updated
            onPressAvatar={this.viewLastReadMessage} // function to execute when user avatar is clicked

            loadEarlier={show_load_earlier} // for loading earlier messages
            onLoadEarlier={this.loadEarlierMessages}
          />
        )}

        // next: add modals      

      </View>
    );

Next, render the modal for showing the fullscreen version of the attached video files:

    <Modal isVisible={is_video_modal_visible}>
      <View style={styles.modal}>
        <TouchableOpacity onPress={this.hideModal.bind(this, 'video')}>
          <Icon name={"close"} size={20} color={"#565656"} style={styles.close} />
        </TouchableOpacity>
        <VideoPlayer uri={video_uri} />
      </View>
    </Modal>

Next, render the modal for showing the last message viewed by a specific member of the room:

    {
      viewed_user && viewed_message &&
      <Modal isVisible={is_last_viewed_message_modal_visible}>
        <View style={styles.modal}>
          <View style={styles.modal_header}>
            <Text style={styles.modal_header_text}>Last viewed msg: {viewed_user}</Text>
            <TouchableOpacity onPress={this.hideModal.bind(this, 'last_viewed_message')}>
              <Icon name={"close"} size={20} color={"#565656"} style={styles.close} />
            </TouchableOpacity>
          </View>

          <View style={styles.modal_body}>
            <Text>Message: {viewed_message}</Text>
          </View>
        </View>
      </Modal>
    }

Next, render the modal for showing the list of members of the room:

    {
      room_users &&
      <Modal isVisible={is_users_modal_visible}>
        <View style={styles.modal}>
          <View style={styles.modal_header}>
            <Text style={styles.modal_header_text}>Users</Text>
            <TouchableOpacity onPress={this.hideModal.bind(this, 'users')}>
              <Icon name={"close"} size={20} color={"#565656"} style={styles.close} />
            </TouchableOpacity>
          </View>

          <View style={styles.modal_body}>
            <FlatList
              keyExtractor={item => item.id.toString()}
              data={room_users}
              renderItem={this.renderUser}
            />
          </View>
        </View>
      </Modal>
    }

Here’s the function for rendering each individual user. This will show a green circle next to the user’s name if they are online, and a gray circle if they’re offline:

    renderUser = ({ item }) => {
      const online_status = item.presenceStore[item.id];

      return (
        <View style={styles.list_item_body}>
          <View style={styles.list_item}>
            <View style={[styles.status_indicator, styles[online_status]]}></View>
            <Text style={styles.list_item_text}>{item.name}</Text>
          </View>
        </View>
      );
    }

Here’s the code for showing the users modal:

    showUsersModal = () => {
      this.setState({
        is_users_modal_visible: true
      });
    }

Lastly, here’s the code for hiding a specific modal:

    hideModal = (type) => {
      const modal = this.modal_types[type];
      this.setState({
        [modal]: false
      });
    }

Attaching files

The code for rendering the custom button for attaching files is already in the starter branch, so all we have to do is add the code for actually attaching a file. To implement this, we use the React Native Document Picker package. We call the DocumentPicker.show function to show the default file picker of the device’s operating system. We use the DocumentPickerUtil.allFiles function to specify that all file types can be selected by the user. You can also supply a specific file type if you want. From their source code, you can see that those methods simply return a mime-type wild card. Though we will only have previews for png, jpg, mp3, and mp4 files:

    openFilePicker = async () => {
      await this.setState({
        is_picking_file: true // show the loader instead of the button for picking files to prevent the user from clicking it again
      });

      DocumentPicker.show({
        filetype: [DocumentPickerUtil.allFiles()],
      }, async (err, file) => {
        if (!err) { // if there's no error in picking the file

          // next: convert the file to a blob
        }

        this.setState({
          is_picking_file: false
        });
      });
    }

Next, we determine the mime type of the file and convert it to its base64 string representation using the react-native-fs package. From there, we convert it to a Blob using the rn-fetch-blob package. As you learned earlier, this acts as a polyfill for implementing Blobs in the React Native environment. Lastly, we temporarily store the name, type, and the Blob representation of the file so that we can easily get it later when we send a message:

    try {
      const file_type = mime.contentType(file.fileName);
      const base64 = await RNFS.readFile(file.uri, "base64");

      const file_blob = await Blob.build(base64, { type: `${file_type};BASE64` });

      this.attachment = {
        file_blob: file_blob,
        file_name: file.fileName,
        file_type: file_type
      };

      Alert.alert("Success", "File attached!");

    } catch (attach_err) {
      console.log("error attaching file: ", attach_err);
    }

Sending messages

The code for sending messages is already included in the starter branch. But we need to update it so that it sends multi-part messages (text with attachment). Replace the existing onSend method with the following. The main difference here is that the text message is included in the message_parts array. This allows us to supply different kinds of content to an individual message. In this case, the other type is an attachment. If the user has selected an attachment, we push it into the message_parts array:

    onSend = async ([message]) => {
      let message_parts = [
        { type: "text/plain", content: message.text }
      ];

      if (this.attachment) {
        const { file_blob, file_name, file_type } = this.attachment;
        message_parts.push({
          file: file_blob, // the file Blob
          name: file_name, // the file name
          type: file_type // the file type
        });
      }

      this.setState({
        is_sending: true // show the loading animation for sending a message
      });

      // next: send message
    }

Next, we send the message. The main difference here is that we’re now using the sendMultipartMessage method instead of the sendSimpleMessage method. Another important thing to note is that when a user sends a message, we also assume that they’ve already read the last message they received. So we set a read cursor using the ID of the last message they received:

    try {
      if (this.last_message_id) {
        const set_cursor_response = await this.currentUser.setReadCursor({
          roomId: this.room_id,
          position: this.last_message_id // the ID of the last message they received
        });
      }
      // send the message
      await this.currentUser.sendMultipartMessage({
        roomId: this.room_id,
        parts: message_parts
      });

      this.attachment = null;
      await this.setState({
        is_sending: false // hide the loading animation
      });
    } catch (send_msg_err) {
      console.log("error sending message: ", send_msg_err);
    }

Receiving messages

The code for receiving messages has already been included in the starter branch, but we need to update it so it sets the last_message_id that we used in the code for sending a message earlier:

    onReceive = async (data) => {
      this.last_message_id = data.id; // add this
      // ...
    }

Next, replace the existing getMessage function with the following:

    getMessage = async ({ id, sender, parts, createdAt }) => {

      const text = parts.find(part => part.partType === 'inline').payload.content;
      const attachment = parts.find(part => part.partType === 'attachment');

      const attachment_url = (attachment) ? await attachment.payload.url() : null;
      const attachment_type = (attachment) ? attachment.payload.type : null;

      const msg_data = {
        _id: id,
        text: text,
        createdAt: new Date(createdAt),
        user: {
          _id: sender.id,
          name: sender.name,
          avatar: `https://ui-avatars.com/api/?background=d88413&color=FFF&name=${sender.name}`
        }
      };

      if (attachment) {
        Object.assign(msg_data, { attachment: { url: attachment_url, type: attachment_type } });
      }

      if (attachment && attachment_type.indexOf('video') !== -1) {
        Object.assign(msg_data, { video: attachment_url });
      }

      if (attachment && attachment_type.indexOf('image') !== -1) {
        Object.assign(msg_data, { image: attachment_url });
      }

      return {
        message: msg_data
      };
    }

We’re using the UI Avatars API to automatically generate an image which matches the user’s initials.
In the code above, the most important distinction is the way we’re getting the text. In this case, the message is already considered a multipart message instead of a simple plain-text message. The first part you specify should always be the text. While the parts that follow can be a file attachment or a URL. The type of a specific part can either be inline, url, or attachment. In this case, we’re handling both inline and attachment types. If you specify, text/plain as the type for a message part, it’s automatically considered as inline. And if you specify a file to a message part, it’s automatically considered as an attachment.

To get to the actual text of an inline message part, we simply extract payload.content:

    const text = parts.find(part => part.partType === 'inline').payload.content;

But for an attachment message part, you have to call the payload’s url() method in order to get the direct URL to the file. This is what we use for previewing the files:

    const attachment = parts.find(part => part.partType === 'attachment');
    const attachment_url = (attachment) ? await attachment.payload.url() : null;

Next, add the renderMessage function. We don’t have this on the starter branch because we’re only rendering plain text previously. This time though, we need to cater for audio (mp3), and video (mp4) files as well. We only render a custom chat bubble if it’s an audio file because Gifted Chat already has a feature for rendering previews for image and video files. If it’s a video though, we supply the onLongPress prop to Gifted Chat’s <Message> component. This allows us to listen for the event when a chat bubble is long-pressed. It’s what we use to render the fullscreen video:

    renderMessage = (msg) => {
      const { attachment } = msg.currentMessage;
      const renderBubble = (attachment && attachment.type.indexOf('audio') !== -1) ? this.renderPreview.bind(this, attachment.url) : null;
      const onLongPress = (attachment  && attachment.type.indexOf('video') !== -1) ? this.onLongPressMessageBubble.bind(this, attachment.url) : null;

      const modified_msg = {
        ...msg,
        renderBubble,
        onLongPress,
        videoProps: {
          paused: true // don't autoplay the video because it's just a preview
        }
      }
      return <Message {...modified_msg} />
    }

When a chat bubble that has a custom onLongPress method is long pressed, the app opens the modal for viewing the fullscreen video:

    onLongPressMessageBubble = (link) => {
      this.setState({
        is_video_modal_visible: true,
        video_uri: link // the direct link to the video
      });
    }

Next, here’s the code for rendering a custom chat bubble. This makes use of the <AudioPlayer> component which has already been pre-coded:

    renderPreview = (uri, bubbleProps) => {
      const text_color = (bubbleProps.position == 'right') ? '#FFF' : '#000';
      const modified_bubbleProps = {
        ...bubbleProps
      };
      return (
        <ChatBubble {...modified_bubbleProps}>
          <AudioPlayer url={uri} />
        </ChatBubble>
      );
    }

Typing indicators

To implement the typing indicators, we use Chatkit’s isTypingIn method to set the room where the current user is currently typing in. This will trigger the onUserStartedTyping hook on the other members of the room who are currently online:

    onTyping = async () => {
      try {
        await this.currentUser.isTypingIn({ roomId: this.room_id });
      } catch (typing_err) {
        console.log("error setting is typing: ", typing_err);
      }
    }

When the onUserStartedTyping hook is triggered, the following function is executed. This shows the chat UI’s footer text which displays the typing indicator:

    startTyping = (user) => {
      this.setState({
        is_typing: true,
        typing_user: user.name
      });
    }

If the user stops typing, the onUserStoppedTyping hook is automatically triggered so the following function is executed as well. This hides the chat UI’s footer text:

    stopTyping = (user) => {
      this.setState({
        is_typing: false,
        typing_user: null
      });
    }

Here’s the code for rendering the custom footer. All it does is show the name of the user who is currently typing in the chat room:

    renderFooter = () => {
      const { is_typing, typing_user } = this.state;
      if (is_typing) {
        return (
          <View style={styles.footerContainer}>
            <Text style={styles.footerText}>
              {typing_user} is typing...
            </Text>
          </View>
        );
      }
      return null;
    }

Read receipt

The last feature that we’re going to implement is the read receipt. This allows the current user to view the last message that another member of the room has read (or get all of the user’s read cursors). This makes use of Chatkit’s readCursor method to fetch the ID of the latest message that a specific user has marked as read (via the setReadCursor method):

    viewLastReadMessage = async (data) => {
      try {
        const cursor = await this.currentUser.readCursor({
          userId: data.userId,
          roomId: this.room_id
        });

        const viewed_message = this.state.messages.find(msg => msg._id == cursor.position);

        await this.setState({
          viewed_user: data.name,
          is_last_viewed_message_modal_visible: true,
          viewed_message: viewed_message.text
        });
      } catch (view_last_msg_err) {
        console.log("error viewing last message: ", view_last_msg_err);
      }
    }

Users can only view the message if it has already been loaded in their chat screen. If the message doesn’t show up then it means that the member they selected hasn’t read any of the recent messages that the current user is able to view.## Running the Chat app

At this point, you can now run the app. Start by running the server and exposing it to the internet using ngrok:

    cd server
    yarn start
    ~/Downloads/ngrok http 5000

Update the login and chat screen with your ngrok URL:

    // src/screens/Login.js, src/screens/Group.js, src/screens/Chat.js
    const CHAT_SERVER = "YOUR NGROK HTTPS URL";

Next, delete the node_modules/react-native-gifted-chat/node_modules/react-native-video folder. This is because we also have React Native Video as a dependency for the app. It’s conflicting with Gifted Chat’s copy, so we need to delete it.

Finally, run the app:

    react-native run-android
    react-native run-ios

Before you log a user in, be sure to visit <a href="http://localhost:5000/users" target="_blank">http://localhost:5000/users</a> on your browser to populate the users array in the server.

Conclusion

In this tutorial, you learned how to use various **Chatkit **features within a React Native app. Specifically, you learned how to implement roles and permissions, read receipts, file uploads, and typing indicators.

You can find the code in this GitHub repo.