Building a Realtime Cryptocurrency App with Flutter
Tags: flutter, dart
Published: 2021-01-21 14:13:46

Learn how to implement realtime messaging in Flutter by building a cryptocurrency app that shows a live dashboard, chat room and Twitter feed.

[Flutter](https://flutter.dev/) is a toolkit made by Google for building cross-platform apps easily. In this tutorial, we’ll show you how to build a realtime cryptocurrency app with 3 screens as described below:

1. **Dashboard screen:** This will be the default home screen where we will display realtime data for cryptocurrency prices from [Ably’s Coindesk](https://www.ably.io/hub/ably-coindesk/crypto-pricing) data stream on the [Ably Hub](https://www.ably.io/hub/) (more on the Hub later). Each currency will have its own line graph that shows the changes in the price over time, along with the actual updated price.

2. **Chat room screen:** This will be the chat screen which you’ll see when the chat icon is clicked. We will create a public chat room where all users who have the app can chat with others who are currently in the room.

3. **Twitter feed screen:** Clicking on the name of any given cryptocurrency on the dashboard will bring up another screen showing the Twitter feed containing the latest tweets with that cryptocurrency mentioned.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pblrvufx3muy9nl6cy41.gif)

The tech world is increasingly moving towards event-driven systems, giving rise to a need for fast and reactive applications. The [Ably Flutter plugin](https://pub.dev/packages/ably_flutter_plugin/) provides a robust and easy way to create Flutter apps with realtime capabilities. It is a wrapper around the Cocoa and Java client library SDKs, providing iOS and Android support for those using Flutter and Dart. We’ll see how to build our app using this.

### 1. Pre-requisites

1. Before we get started, please make sure that you have Flutter correctly installed on your machine. You can do it by following the [Flutter installation guide](https://flutter.dev/docs/get-started/install).

2. Project files:
  - You can start from scratch and follow along the tutorial by creating a new Flutter project in the desired location, and remove all unnecessary code: 

```shell
flutter create live-cryptocurrency-streaming-app
```

  - or [clone the github repo](https://github.com/ably-labs/live-cryptocurrency-streaming-flutter) that already has the full project.

3. Add the packages mentioned in step [1.4. Packages and dependencies](#packages) to `pubspec.yaml` file and run: 

```shell
flutter pub get
```

4. The next step is to [sign up](https://www.ably.io/signup) for a free Ably account to obtain an **Ably API key**. This is needed to make the Ably Flutter plugin work.

5. Ably has a set of streaming data sources that can be used free of charge in your apps and services. They are hosted on the [Ably Hub](https://www.ably.io/hub). For our application, we’ll make use of the [**Cryptocurrency pricing**](https://www.ably.io/hub/ably-coindesk/crypto-pricing) data stream. At the time of this writing, it supports the BTC, XRP, and ETH currencies and shows the corresponding prices in USD. Go ahead and click on the _subscribe_ button to get access to this data stream from your Ably account.

6. Next you need to [sign up](https://developer.twitter.com/) for a Twitter developer account to get Twitter API keys. This is needed to get the Twitter feed screen working. It’s not necessary as the application as a whole will still work even with the Twitter feed missing.

### 1.1. Project Files Structure

```
Lib // root folder of all dart files in a Flutter app
|_ service
|____ ably_service.dart
|____ twitter_api_service.dart
|_ view
|____ dashboard.dart
|____ twitter_feed.dart
|____ chat.dart
|_ config.dart
|_ main.dart
```

This is how our project’s structure will look like. We'll keep the UI separate from the data source by creating services. Go ahead and create these as empty files for now.

The most important file and the main focus of this tutorial will be the `ably_service.dart` file. This is where we will write all the code to communicate with Ably realtime.

If you cloned the full project, you would notice the `config_example.dart` file, which has a few constants to hold the secret keys for Ably and Twitter APIs. Please paste your keys from the previous steps here, and rename the file to `config.dart`, you will also find notes to guide you inside the file.

In case you are starting from scratch, create a new file named `config.dart` and paste your keys in it as follows:

```dart
const String AblyAPIKey = 'Your Ably API Key goes here';

// The following keys should be taken from your Twitter developer account
const String OAuthConsumerKey = '';
const String OAuthConsumerSecret = '';
const String OAuthToken = '';
const String OAuthTokenSecret = '';
```

**IMPORTANT:** We highly recommend not to commit this file into a public github repository. So, make sure to immediately add this to your `.gitignore` file, if it's not already there.
<a name="packages"></a>
### 1.2. Packages and dependencies

In Flutter, we can make use of third-party packages to add extra functionality and make it easier to do many things without needing to re-invent the wheel. The "pub.dev":pub.dev website has a list of all the packages you can use in your Flutter projects. To use a package we just have to add the package name and version in the `pubspec.yaml` file as follows. Go ahead and add these in your file.

```yaml
dependencies:
  flutter:
    sdk: flutter
    
  ably_flutter_plugin: ^1.0.0+dev.2
  get_it: ^5.0.1
  syncfusion_flutter_charts: ^18.3.52
  http: ^0.12.2
  intl: ^0.16.1
  tweet_ui: ^2.4.2
  twitter_api: ^0.1.2
```

1. [ably_flutter_plugin](https://pub.dev/packages/ably_flutter_plugin):
Ably’s Flutter package is a wrapper around the existing iOS and Android packages to provide scalable pub/sub messaging infrastructure out of the box. We will use it to connect to the Ably realtime service. 

2. [get_it](https://pub.dev/packages/get_it)
We’ll use the get_it package for locating the services and using them in the UI classes. It’s a popular package used to manage state in a Flutter app and to separate our business logic from the UI. We will see how to use it to connect our services with the views later in the tutorial.

3. [syncfusion_flutter_charts](https://pub.dev/packages/syncfusion_flutter_charts)
Syncfusion provides a wide range of packages for Flutter, this charts package is easy to use and can be highly customized. We will use it for the charts in the dashboard page.

4. [intl](https://pub.dev/packages/intl)
The most popular internationalization package for Flutter, we will use it for dates formatting.

5. [twitter_api](https://pub.dev/packages/twitter_api)
Twitter has a complicated way of setting up a request, to simplify the process we will use this package which abstracts away that complexity for us.

6. [http](https://pub.dev/packages/http)
As we will connect to the Twitter API in one of the screens, this popular package provides us with an easy way to send HTTP requests. However, as you will see later, we won’t be using this package to send HTTP requests but only to work around a small issue with the twitter_api package.

7. [tweet_ui](https://pub.dev/packages/tweet_ui):
A ready-made widget to display different types of tweets by simply passing the relevant json data.


## 2. Building the Realtime Cryptocurrency Charts

Let's go back to the [Cryptocurrency prices hub](https://www.ably.io/hub/ably-coindesk/crypto-pricing) page on the Hub that you subscribed to in the previous steps. Each cryptocurrency has a display name and a code. Also, each currency has a unique channel in the Hub. We'll use the code to connect to the specific channel for the particular currency. The display name is what the user will see when using the app.

Inside the `ably_service.dart` file, we will start with a `const List` variable that will store the currently available currencies on the Hub. If any new currency is added to the source, we can append it to this list and the whole app will be updated.

```dart
const List<Map> _coinTypes = [
  {
    "name": "Bitcoin",
    "code": "btc",
  },
  {
    "name": "Ethereum",
    "code": "eth",
  },
  {
    "name": "Ripple",
    "code": "xrp",
  },
];
```

### 2.1. Cryptocurrency Data Model

We need a model to hold the coin information and deliver it to the UI code. Instead of sending the raw data received from Ably immediately, we will use this model to `map` the received data. This will improve the readability of the code and completely separate out the service layer.

```dart
class Coin {
  final String code;
  final double price;
  final DateTime dateTime;

  Coin({
    this.code,
    this.price,
    this.dateTime,
  });
}
```

### 2.2. Realtime Service Class

Let’s create the main service class `AblyService`, and initialize it with a private constructor.

```dart
Class AblyService {
  AblyService._(this._realtime);
}
```

The reason we do this is because we want this service to be a [_Singleton_](https://en.wikipedia.org/wiki/Singleton_pattern) i.e. initialized only **once** at the time of launching the app. 

We don't want to initialize a new instance of the service each time we need to access its methods. Instead, we need all the methods to use the same connection and instance information. 

To initialize our service, we will write a special `static` method. We'll create and return the private `instance` of this class that can be used anywhere in the app. We'll also add the configuration necessary to establish a realtime connection to Ably upon first initialization.

```dart
static Future<AblyService> init() async {
    /// initialize client options for your Ably account using your private API
    /// key
    final ably.ClientOptions _clientOptions =
        ably.ClientOptions.fromKey(APIKey);

    /// initialize real-time object with the client options
    final _realtime = ably.Realtime(options: _clientOptions);

    /// connect the app to Ably's Realtime services supported by this SDK
    await _realtime.connect();

    /// return the single instance of AblyService with the local _realtime
    /// instance to
    /// be set as the value of the service's _realtime property, so it can be 
    /// used in all methods.
    return AblyService._(_realtime);
}
```

Let’s take a moment to understand what we did here. You can see that we passed the local `_realtime` property to the constructor of the `AblyService` class which will set the class-level `_realTime` property allowing other methods to use it.

Let’s now connect to the cryptocurrency channel and subscribe to the coin prices. For this, we will add a method called `getCointUpdates()`. This method will establish the connection, listen to the `stream` of messages coming from Ably, and `map` each message to a `Coin` object.

```dart
List<CoinUpdates> _coinUpdates = [];

List<CoinUpdates> getCoinUpdates() {
    if (_coinUpdates.isEmpty) {
      for (int i = 0; i < _coinTypes.length; i++) {
        String coinName = _coinTypes[i]['name'];
        String coinCode = _coinTypes[i]['code'];

        _coinUpdates.add(CoinUpdates(name: coinName));

        //launch a channel for each coin type
        ably.RealtimeChannel channel = _realtime.channels
            .get('[product:ably-coindesk/crypto-pricing]$coinCode:usd');

        //subscribe to receive a Dart Stream that emits the channel messages
        final Stream<ably.Message> messageStream = channel.subscribe();

        //map each stream event to a Coin and listen to updates
        messageStream.where((event) => event.data != null).listen((message) {
          _coinUpdates[i].updateCoin(
            Coin(
              code: coinCode,
              price: double.parse('${message.data}'),
              dateTime: message.timestamp,
            ),
          );
        });
      }
    }
    return _coinUpdates;
}
```

Let's understand the code above. We iterate over the constant `_coinTypes` list that we created before. For each coin type in the list, we obtain and subscribe to the relevant Ably channel. Each such channel contains a Dart @Stream@ emitting new events as they are published on the channel. You can read more on [Dart Streams](https://api.dart.dev/stable/2.10.4/dart-async/Stream-class.html) to get a better understanding.

### 2.3. Notifying the UI of New Data

To consume the data stream easily in the UI, we will create a new class that extends `ChangeNotifier` interface, which is the simplest way in Flutter to get notified when anything changes. You can think of it as the messenger responsible for delivering each new message emitted from the stream to the UI.

```dart
class CoinUpdates extends ChangeNotifier {
  CoinUpdates({this.name});
  final String name;

  Coin _coin;

  Coin get coin => _coin;
  updateCoin(value) {
    this._coin = value;
    notifyListeners();
  }
}
```

Any UI widget that registers a listener for this object will get a notification whenever it has to rebuild with new data.

The update will happen when calling the `updateCoin()` method, which will assign the new coin data to `_coin`, then call the `notifyListeners()` method.

We chose to transform `Stream` events into `ChangeNotifier` updates because they are much easier to use in the UI as they always have a valid value and won't complain if they have multiple subscriptions.

### 2.4. Subscribe to Ably Channels

Let’s break down the previous function and understand the subscription step in detail:

* Get the realtime channel relevant to the data stream we are interested in. 
```dart
ably.RealtimeChannel channel = _realtime.channels.get('[product:ably-coindesk/crypto-pricing]$coinCode:usd');
```
In a real App it’s probably a good idea to check if we got the channel that we requested.

* Subscribe to that channel
```dart
final Stream<ably.Message> messageStream = channel.subscribe();
```

* The returned type from `subscribe()` is a `Stream<Message>`, which feels a bit odd because we just subscribed to something. In reality the `subscribe` tells Ably to start transmitting data.

* So we register a listener for this message Stream, and use `where` method to filter null values.
```dart
messageStream.where((event) => event.data != null).listen((message) {});
```

As we never stop listening to the channels in this app, we can ignore the `StreamSubscription` object that is returned from `listen()`.

Inside the listener, whenever a new message arrives, we call the `updateCoin()` method and pass it a new `Coin` mapped from the `Message` data.

The return type of this function is a `List<CoinUpdates>` which has the same length as `_coinTypes` list and with a `CoinUpdates` object for every currency defined in `_coinTypes`. 

To be safe in case this function is called more than once, we wrap the `for` loop in an `if` condition that checks if the channel subscriptions already exists.

We have now finished setting up our service, it’s time to see how we will use it to show the prices graphs. 

To be able to access our services easily we use the service locator package `get_it`. Feel free to use `package/provider` or any other solution that you are comfortable with.

The following diagram visualizes how the data will flow from Ably to our App’s UI.

![data-flow](https://dev-to-uploads.s3.amazonaws.com/i/7xm4qqtfmk768kg1fuwm.png)


### 2.5. Registering Services with get_it

The first step is to register the service using `get_it` package. We will do that asynchronously in the `main` method, as we want this service to be available as soon as the app is launched.

```dart
GetIt getIt = GetIt.instance;

void main() {
  getIt.registerSingletonAsync<AblyService>(() => AblyService.init());
  runApp(MyApp());
}
```

Check out the [package documentation on GitHub](https://github.com/fluttercommunity/get_it#asynchronous-singletons) for more information on how asynchronous registration with `get_it` works.

As this is an asynchronous registration, it won’t be available to our UI immediately. Hence, we will wait for it to become available before using it. For this we will use a `FutureBuilder` widget which will show a loading spinner until `get_it` reports that all services are ready.

Inside `dashboard.dart` file, make a `StatelessWidget` and paste the following code inside the `build()` method.

```dart
return Scaffold(
  appBar: AppBar(
    title:
        Text(
          "Live cryptocurrency by Ably", 
          style: TextStyle(fontSize: 16),
        ),
    actions: [
      IconButton(
        icon: Icon(Icons.chat_bubble),
        onPressed: () => _navigateToChatRoom(context),
      )
    ],
    bottom: PreferredSize(
      child: Container(
        color: Colors.white,
        height: 1.0,
      ),
      preferredSize: Size.fromHeight(1.0),
    ),
  ),
  body: FutureBuilder(
    future: getIt.allReady(),
    builder: (context, snapshot) {
      if (!snapshot.hasData)
        return Center(child: CircularProgressIndicator());
      else
        return GraphsList();
    },
  ),
);
```

### 2.6. Listening to Cryptocurrency Prices

Now that we are sure the service will be ready at the time we use it, make a new `StatefulWidget` with the name `GraphsList` so that we can register a listener in the `initState()` method to listen to the cryptocurrency prices.

```dart
List<CoinUpdates> prices = [];

@override
void initState() {
    prices = getIt<AblyService>().getCoinUpdates();
    super.initState();
}
```

On initial load of this page, the prices will not be ready because the app is most likely establishing a connection with Ably. To manage this, we'll need the service to tell us what the current connection status is. 

For this let's get back to `AblyService` class and add a new property called `connection` of type `Stream`. This will report any changes to our connection status to Ably.

```dart
Stream<ably.ConnectionStateChange> get connection => _realtime.connection.on();
```

Since it’s a `Stream` object, we will use a `StreamBuilder` widget inside `GraphsList` widget to read the connection status, and then decide what to display accordingly.

Back to `GraphsList` widget, add the following code to the `build()` method.

```dart
StreamBuilder<ably.ConnectionStateChange>(
  // As we are behind the FutureBuilder we can safely access AblyService 
  stream: getIt<AblyService>().connection,
  builder: (context, snapshot) {
    if (!snapshot.hasData) {
      return CircularProgressIndicator();
    } else if (snapshot.data.event == ably.ConnectionEvent.connected) {
      // return the list of graphs, 
        SingleChildScrollView(
          // see section below
        );
    } else if (snapshot.data.event == ably.ConnectionEvent.failed) {
      return Center(child: Text("No connection."));
    } else {
      // In a real app we would also add handling of possible errors
      return CircularProgressIndicator();
    }
  },
),
```

Now that all cases are handled, let’s display the list of charts if the connection is successful.

### 2.7. Displaying Charts with Real Data

```dart
SingleChildScrollView(
	child: Column(
		children: [
			for (CoinUpdates update in prices)
   CoinGraphItem(coinUpdates: update),
		 ],
	),
),
```
Instead of using a `ListView.builder` widget, we have just used a `Column` widget with a `for-collection` operation. In this case, using a `Column` widget is more convenient since the `ListView`, by default, will dispose off the states of any child that isn’t visible anymore. That's good behaviour in case a list is very long, but since we know that the number of graphs is limited and don’t want them to be disposed off or rebuilt each time the user scrolls up or down, a `Column` should work fine.

Each `CoinGraphItem` widget will require `CoinUpdates`. As it is extending the `ChangeNotifier`, it will register a listener for price updates and push each new price update into a `Queue`.

We can't use a `List` here because the size of the list will become huge very quickly needing too many resources. We don’t really have to show all the historical prices but just the last 100. Using a `Queue` would make it easy to remove the first item if the length exceeds the required length.

```dart
Queue<Coin> queue = Queue();
String coinName = '';

VoidCallback _listener;

@override
void initState() {
    widget.coinUpdates.addListener(
      _listener = () {
        setState(() {
          queue.add(widget.coinUpdates.coin);
        });

        if (queue.length > 100) {
          queue.removeFirst();
        }
      },
    );

    if (coinName.isEmpty) coinName = widget.coinUpdates.name;

    super.initState();
}
```

To be safe, it’s always good practice to cancel any listeners at disposal:

```dart
@override
void dispose() {
    widget.coinUpdates.removeListener(_listener);
    super.dispose();
}
```

We are all set up and ready! Now we just need the queue to be turned into a list so that the graph can start rendering the data. We'll show price on the Y axis, and time on the X axis.

Here we will use the [**Syncfusion Flutter Charts**](https://www.syncfusion.com/flutter-widgets/flutter-charts) package to render the charts. Why Syncfusion? Their Flutter Charts package is a beautifully-crafted charting widget to visualize data. It contains a gallery of 30+ charts and graphs that can be fully customized with options to include animations and render huge amounts of data in seconds. You can [try it yourself for free](https://www.syncfusion.com/account/login).

![Syncfusion charts used in the Dashboard](https://dev-to-uploads.s3.amazonaws.com/i/0af1ozs8f3xx4w3nv6vu.png)

```dart
@override
Widget build(BuildContext context) {
  return Container(
    margin: EdgeInsets.all(15),
    padding: EdgeInsets.all(15),
    height: 410,
    decoration: BoxDecoration(
        color: Color(0xffEDEDED).withOpacity(0.05),
        borderRadius: BorderRadius.circular(8.0)),
    child: AnimatedSwitcher(
      duration: Duration(milliseconds: 500),
      child: queue.isEmpty
          ? Center(
              key: UniqueKey(),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  CircularProgressIndicator(),
                  SizedBox(
                    height: 24,
                  ),
                  Text('Waiting for coin data...')
                ],
              ),
            )
          : Column(
              key: ValueKey(coinName),
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    FlatButton(
                      onPressed: () => _navigateToTwitterFeed(coinName),
                      textColor: Colors.white,
                      child: Row(
                        children: [
                          Image.asset(
                            'assets/icon_awesome_twitter.png',
                            height: 20,
                          ),
                          SizedBox(width: 10),
                          Text(
                            "#$coinName",
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 20,
                            ),
                          ),
                        ],
                      ),
                    ),
                    AnimatedSwitcher(
                      duration: Duration(milliseconds: 200),
                      child: Text(
                        "\$${widget.coinUpdates.coin.price.toStringAsFixed(2)}",
                        key: ValueKey(widget.coinUpdates.coin.price),
                        style: TextStyle(
                          fontSize: 20,
                        ),
                      ),
                    ),
                  ],
                ),
                SizedBox(height: 25),
                SfCartesianChart(
                  enableAxisAnimation: true,
                  primaryXAxis: DateTimeAxis(
                    dateFormat: intl.DateFormat.Hms(),
                    intervalType: DateTimeIntervalType.minutes,
                    desiredIntervals: 10,
                    axisLine: AxisLine(width: 2, color: Colors.white),
                    majorTickLines: MajorTickLines(color: Colors.transparent),
                  ),
                  primaryYAxis: NumericAxis(
                    numberFormat: intl.NumberFormat('##,###.00'),
                    desiredIntervals: 5,
                    decimalPlaces: 2,
                    axisLine: AxisLine(width: 2, color: Colors.white),
                    majorTickLines: MajorTickLines(color: Colors.transparent),
                  ),
                  plotAreaBorderColor: Colors.white.withOpacity(0.2),
                  plotAreaBorderWidth: 0.2,
                  series: <LineSeries<Coin, DateTime>>[
                    LineSeries<Coin, DateTime>(
                      animationDuration: 0.0,
                      width: 2,
                      color: Theme.of(context).primaryColor,
                      dataSource: queue.toList(),
                      xValueMapper: (Coin coin, _) => coin.dateTime,
                      yValueMapper: (Coin coin, _) => coin.price,
                    )
                  ],
                )
              ],
            ),
    ),
  );
}
```

With that we finished building the first page, fully functional with realtime updates from the [Ably Cryptocurrency](https://www.ably.io/hub/ably-coindesk/crypto-pricing) data stream on the (Hub)[https://www.ably.io/hub].

## 3. Building the Flutter Chat Room

In the previous section, we subscribed to a channel as a consumer of that public data stream. As we don't have publish rights on that data stream, we can’t add data to it. In this section, we will take a look at how to create private channels programmatically, subscribe to them as well as publish messages. 

Building the chat room with Ably realtime capabilities is fairly simple! In our `AblySevice` class, we will add two methods, one to listen to the latest messages as long as a user is on the chat room screen, and the another method to send new messages.

```dart
ChatUpdates getChatUpdates() {
  ChatUpdates _chatUpdates = ChatUpdates();

  _chatChannel = _realtime.channels.get('public-chat');

  var messageStream = _chatChannel.subscribe();

  messageStream.listen((message) {
    _chatUpdates.updateChat(
      ChatMessage(
        content: message.data,
        dateTime: message.timestamp,
        isWriter: message.name == "${_realtime.clientId}",
      ),
    );
  });

  return _chatUpdates;
}
```

Channel names are unique for a specific Ably app, so channels of different apps can have the same name (if you want to send messages from one app to the other you can do this by using the [API Streamer](https://www.ably.io/api-streamer/) ). For our chat, we will use the channel name `public-chat`. We'll use this channel to send and receive realtime chat messages.

If the channel doesn’t exist at the time this function is called, it will be created as a new one automatically.

Just like we did previously with prices, we will create a `ChatUpdates` class as a `ChangeNotifier` holding the most recently published message and push it to a queue in the UI. We'll call the `getChatUpdates()` method on the same instance of `AblyService` that we previously registered with the `get_it` package. Doing this will subscribe our app to the chat channel enabling it to receive updates whenever a new message is published on that channel.

For publishing our messages, we will add the `sendMessage()` method to the service.

```dart
Future sendMessage(String content) async {
  _realtime.channels.get('public-chat');

  await _chatChannel.publish(data: content, name: "${_realtime.clientId}");
}
```

To publish messages, we call the `publish` method on the chat `channel` instance. The `name` parameter in `publish` method is optional, it can be used to differentiate various types of messages that are sent over the same channel. We set the event name in the publish method as clientID of the connected device. This will enable us to differentiate the messages sent by the current user from the messages sent by others on the same chat channel.

To avoid unnecessary code for this demo app, we don’t store the clientIDS anywhere. This means that you will have a new clientID every time you start the app.

With this our chat infrastructure in the service is done. So lets move to the chat view. Once we open it, we need it to initialize a listener, just like we did on the main page `DashboardView`.

In `chat.dart` file, we will initialize the channel and set up our listener:

```dart
Queue<ChatMessage> messages = Queue();
ChatUpdates chatUpdates;
VoidCallback _listener;

@override
void initState() {
  super.initState();
  
  chatUpdates = getIt<AblyService>().getChatUpdates();

  chatUpdates.addListener(
    _listener = () {
      if (chatUpdates.message != null)
        setState(() {
          messages.addFirst(chatUpdates.message);
        });
      if (messages.length > 100) {
        messages.removeFirst();
      }
    },
  );
}
```

As we have the client ID sent with each message, we know if a message is coming from the current user or from other users on the same channel. We can change the look of the message bubble accordingly.

To display the chat bubbles we will use a `ListView.builder()` widget. As messages should appear in the reverse order, the first item in the messages queue is always the most recent message. So we will display the items in reverse order so that the first message always appears at the bottom.

```dart
Flexible(
  child: ListView.builder(
    reverse: true,
    itemCount: messages.length,
    itemBuilder: (context, index) {
      return ChatMessageBubble(
        message: messages.toList()[index],
        isWriter: messages.toList()[index].isWriter,
      );
    },
  ),
),
```

Each list item is a message, so we will create a custom widget to render a message bubble. The message bubble itself will have two different looks, one when the message is from the current user, and another for messages from other users.

```dart
class ChatMessageBubble extends StatelessWidget {
  const ChatMessageBubble({
    Key key,
    this.message,
    this.isWriter = false,
  }) : super(key: key);
  final ChatMessage message;
  final bool isWriter;

  final double radius = 15;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(15),
      child: Column(
        crossAxisAlignment:
            isWriter ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: [
          Container(
            padding: EdgeInsets.all(10),
            alignment: Alignment.centerLeft,
            decoration: BoxDecoration(
              color: isWriter
                  ? Theme.of(context).primaryColor.withOpacity(0.5)
                  : Colors.white12,
              borderRadius: BorderRadius.only(
                bottomLeft: Radius.circular(isWriter ? radius : 0),
                bottomRight: Radius.circular(isWriter ? 0 : radius),
                topLeft: Radius.circular(radius),
                topRight: Radius.circular(radius),
              ),
            ),
            width: MediaQuery.of(context).size.width * 0.6,
            constraints: BoxConstraints(minHeight: 50),
            child: Text(message.content),
          ),
          SizedBox(height: 5),
          Align(
            alignment: isWriter ? Alignment.bottomRight : Alignment.bottomLeft,
            child: Text(
              intl.DateFormat.Hm().format(message.dateTime),
              style: TextStyle(color: Colors.white24),
              textAlign: TextAlign.left,
            ),
          )
        ],
      ),
    );
  }
}
```

We that we've fully implemented the chat screen. Let's move on to the final one.

## 4. Viewing Recent Tweets for Each Coin

To display Tweets that have a hashtag of the coin name, we will create a second service to connect to the Twitter API. It’s a good practice to separate different data sources into separate service classes. 

Before starting, it's worth noting that this tutorial is using **Twitter API 1.0**.

If you want to try this part in your own app you will have to [register for a Twitter developer account](https://developer.twitter.com/en/apply-for-access) to get your private API keys. When you log into your Twitter developer account, you can generate your keys in the developer console. Copy them out and add them to the `config.dart` file.

To query the Twitter API manually using an HTTP request is a bit complex as it requires a lot of calculations to get the signature for each request. To make our life a bit easier, we will use the [twitter_api](https://pub.dev/packages/twitter_api) and [tweet_ui](https://pub.dev/packages/tweet_ui) packages to display the tweets.

It's worth noting that the `twitter_api` package that deals with all the signature and authorization details only works for v1.0 API of Twitter. Please note that you can still implement the API access using Dart only.

You can now switch to the `twitter_api_service.dart` file. Before we can use the `twitterApi` method, we will have to initialize it with all the required keys:

```dart
TwitterAPIService({this.queryTag}) {
  _twitterApi = twitterApi(
    consumerKey: OAuthConsumerKey,
    consumerSecret: OAuthConsumerSecret,
    token: OAuthToken,
    tokenSecret: OAuthTokenSecret,
  );
}
```

To get the recent tweets we will use the standard Twitter search endpoint:

```dart
static const String path = "search/tweets.json";
```

With the `twitterApi` instance initialized with our keys, it’s time to request tweets based on the hashtag passed through the constructor:

```dart
Future<List> getTweetsQuery() async {
  try {
    // Make the request to twitter
    Response response = await _twitterApi.getTwitterRequest(
      // Http Method
      "GET",
      // Endpoint you are trying to reach
      path,
      // The options for the request
      options: {
        "q": queryTag,
        "count": "50",
      },
    );

    final decodedResponse = json.decode(response.body);

    return decodedResponse['statuses'] as List;
  } catch (error) {
    rethrow;
  }
}
```

This time the response won’t be of type `Stream`, but a `Future`. It uses the `http` package under the hood, so the returned type from the request is an http `Response` object which needs to be decoded. This is the reason we explicitly imported the `http` package - to give a type to the response. We could proceed without it with just `final response`, but it’s a good practice in Flutter and Dart to always be explicit with types.

```dart
final decodedResponse = json.decode(response.body);
```

The response body is a `Map` object. All the tweet data is inside a list, and this list has a key called `statuses`, that’s why the returned value is `decodedResponse['statuses']`.

Let's now switch to the `twitter_feed.dart` file to implement the UI. As mentioned before, we will use the `tweet_ui` package to display the Tweets in their familiar design.

You don’t have to use this package and you can always implement your own widget for the tweets if you like.

We could have registered the Twitter service via `get_it` too but as we always create the Tweets page dynamically without needing to persist data, we can create a new instance everytime the `TwitterfeedView` is pushed.

To do this, we first initialize a service instance in `getTweets()` method using the hashtag that was passed from the dashboard. Then, we will call the `getTweetsQuery()` method. As it returns a `Future`, we need to `await` the result. When the result is ready, we update the local state of the widget using `setState` which will call the build method to switch from displaying a loading spinner to the actual list of tweets.

We can’t do this directly inside @initState()@ because this API is asynchronous, and `initState()` method can’t be defined as an async function. We will use a separate async method called `getTweets()` that we will call from the `initState()` method. We can do this safely as we have already ensured that the page will render correctly even with no data received.

```dart
Future getTweets() async {
  final twitterService = TwitterAPIService(queryTag: widget.hashtag);

  try {
    final List response = await twitterService.getTweetsQuery();

    setState(() {
      tweetsJson = response;
    });
  } catch (error) {
    setState(() {
      errorMessage = 'Error retrieving tweets, please try again later.';
    });
  }
}
```

If any exception is rethrown by the service, which could happen if you don’t use valid keys or if there is a network problem, it will display a nice error message without the app pausing on the exception.

That's all! We have implemented all the three screens in our Flutter cryptocurrency app.

## 5. Conclusion and next steps

* If you would like to read more on Ably’s realtime services, [read the official documentation](https://www.ably.io/documentation/realtime/channels/) which shows example code snippets for Flutter and offers deeper explanations on various concepts.

* The [full source code for this project is available on GitHub](https://github.com/ably-labs/live-cryptocurrency-streaming-flutter).

* Understand the client-side considerations when [building realtime apps with Flutter and WebSockets](https://www.ably.io/topic/websockets-flutter).

* Read more on [Simple Flutter app state management with ChangeNotifier](https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple).

* Read more on [Using Flutter packages](https://flutter.dev/docs/development/packages-and-plugins/using-packages).

* Learn more about [Dart Streams](https://www.youtube.com/watch?v=nQBpOIHE4eE) in their official YouTube video.

* If you have any questions, feel free to [reach out to Ably](https://www.ably.io/contact) and they’ll be happy to help you out.

* You can follow the roadmap and request new features directly on [Ably Flutter plugin’s GitHub repo](https://github.com/ably/ably-flutter).