ably.com Open in urlscan Pro
2606:4700:10::6814:59c3  Public Scan

URL: https://ably.com/tutorials/realtime-cryptocurrency-app-flutter
Submission: On February 10 via api from BY — Scanned from DE

Form analysis 3 forms found in the DOM

GET /search

<form class="flex items-start" action="/search" method="get">
  <div class="relative w-full">
    <svg class="text-cool-black absolute top-12 left-16" style="width: 1.5rem; height: 1.5rem">
      <use xlink:href="#sprite-icon-gui-search"></use>
    </svg>
    <input type="search" name="q" class="ui-input pl-48 h-48" placeholder="Search" autocomplete="off" data-id="meganav-search-input">
    <div class="absolute w-full mt-8 z-10 hidden shadow-container rounded-lg bg-white border border-mid-grey" data-id="meganav-search-autocomplete-container">
      <ol class="m-16" data-id="meganav-search-autocomplete-list"></ol>
    </div>
  </div>
  <button type="submit" class="ui-btn-secondary flex-shrink-0 ml-8 sm:ml-16 md:ml-24 xl:ml-32"> Search </button>
</form>

GET /search

<form class="mb-16" action="/search" method="get">
  <div class="relative w-full">
    <svg class="text-cool-black absolute top-12 left-16 hover:text-gui-hover" style="width: 1.5rem; height: 1.5rem">
      <use xlink:href="#sprite-icon-gui-search"></use>
    </svg>
    <button type="button" class="absolute top-12 right-16 p-0 focus:outline-gui-focus m-0 md:hidden invisible" data-id="meganav-search-input-clear">
      <svg class="text-dark-grey " style="width: 1.5rem; height: 1.5rem">
        <use xlink:href="#sprite-icon-gui-cross-circled-fill"></use>
      </svg>
    </button>
    <input type="search" name="q" class="ui-input px-48 h-48" style="{{" maxwidth:="" "none"="" }}="" placeholder=" Search" autocomplete="off" data-id="meganav-mobile-search-input">
    <div class="absolute w-full mt-8 z-10 hidden shadow-container rounded-lg bg-white border border-mid-grey" data-id="meganav-search-autocomplete-container">
      <ol class="m-16" data-id="meganav-search-autocomplete-list"></ol>
    </div>
  </div>
</form>

<form id="modal-newsletter-form" class="c-form">
  <h4 class="u-fs-3 u-m-b-small">Data streaming tutorials, realtime insights, and Ably announcements straight to your inbox</h4>
  <input type="email" name="EMAIL" id="newsletter-popout-email" placeholder="Your email address" class="c-input c-input--primary u-m-b-tiny">
  <input type="submit" value="Subscribe" name="subscribe" id="newsletter-popout-subscribe" class="c-button c-button--primary c-button--block">
</form>

Text Content

ai-ably-spinner ai-ably-symbol ai-ably-symbol-layered ai-aim Created by
iconfieldfrom the Noun Project ai-antenna ai-antenna-alt ai-arrow-left
ai-arrow-left-o ai-arrow-long-down ai-arrow-long-up ai-arrow-right
ai-arrow-right-o ai-arrows-horizontal ai-arrows-horizontal-fill ai-award
ai-balloon ai-barricade ai-bell icon-ui-firehose Created with Sketch. ai-box
ai-bug ai-business-card ai-calculator ai-calculator-lg ai-chat ai-chat-bubble
ai-chat-bubble-writing ai-chats ai-chevron-down ai-chevron-up ai-circuit
ai-clock ai-close-hairline ai-close-rounded ai-cloud ai-cloud-pull ai-cloud-push
ai-cog ai-cogs ai-dashboard ai-database ai-db-alt ai-destroy-doc
ai-destroy-doc-alt ai-doc-download ai-doc-upload ai-door ai-download ai-drawer
ai-enterprise ai-envelope ai-eu ai-event ai-exchange ai-exchange-fill ai-expand
ai-eye icon-ui-firehose Created with Sketch. ai-flag-triangle ai-foldable-map
ai-gas-station ai-ghost-o ai-gift ai-github ai-glasses ai-globe-wired
ai-google-multilayer ai-gym-bar ai-headset ai-heart ai-help ai-key ai-layers
ai-libraries ai-library ai-lightbulb ai-lightning ai-link ai-lock ai-lock-alt
ai-magic-wand ai-magnet ai-map-pin ai-meter ai-minus ai-minus-o-circle ai-mug
ai-multi-docs ai-navigation ai-notification ai-palette ai-paper ai-pen ai-phone
ai-pie-chart ai-pig ai-pill ai-pin ai-play ai-play-grey ai-plug ai-plugging
ai-plus-o ai-plus-o-circle ai-pointer ai-power-btn ai-pusher-symbol
ai-question-o-circle ai-robot ai-rocket ai-schematics-1 ai-schematics-10
ai-schematics-11 ai-schematics-12 ai-schematics-13 ai-schematics-14
ai-schematics-15 ai-schematics-2 ai-schematics-3 ai-schematics-4 ai-schematics-5
ai-schematics-6 ai-schematics-7 ai-schematics-8 ai-schematics-9
ai-search-rounded ai-search-rounded-brand ai-send ai-separator-arrow-rounded
icon-ui-serverless-funcs Created with Sketch. ai-settings ai-shield ai-smiley
ai-star icon-ui-data-streams Created with Sketch. ai-system-on Created with
Sketch. ai-tags ai-target ai-thumbs-up ai-tick ai-tick-brand ai-tick-outline
ai-time-history ai-tools ai-twitter ai-undo ai-upload ai-user icon-ui-webhooks
Created with Sketch. ai-wrench

How we use cookies to improve your experience.

Accept and close
Your browser has Javascript disabled. Please enable it to use this site. Hide
this warning
 * Products Products
   
   
   THE ABLY PLATFORM
   
   Easily power any realtime experience in your application. No complex
   infrastructure to manage or provision. Just a simple API that handles
   everything realtime, and lets you focus on your code.
   
   Explore how it works
   
   
   PRODUCTS
   
    * Pub/Sub Channels
      
      Build infinitely scalable realtime applications.
   
    * Spaces (Beta)
      
      Create multi-user collaborative environments.
   
    * LiveSync (Pre release)
      
      Keep clients in sync with any relational database.
   
   
   TECHNOLOGY
   
    * Predictable performance
      
      A low-latency and high-throughput global network.
   
    * Guaranteed ordering & delivery
      
      Data is delivered - in order - even after disconnections.
   
    * Fault tolerant infrastructure
      
      Redundancy is built in at global and regional levels.
   
    * High scalability & availability
      
      Built for scale with legitimate 99.999% uptime SLAs.
   
    * Global edge network
      
      An edge network of 15 core routing datacenters and 205+ PoPs.
   
   Explore Four Pillars of Dependability
   
 * Solutions Solutions
   
   
   SOLUTIONS
   
    * Live Chat
      
      Deliver highly reliable chat experiences at scale.
   
    * Multiplayer Collaboration
      
      Bring collaborative multiplayer experiences to your users.
   
    * Data Broadcast
      
      Broadcast realtime event data to millions of devices around the globe.
   
    * Data Synchronization
      
      Keep your frontend and backend in realtime sync, at global scale.
   
    * Notifications
      
      Deliver cross-platform push notifications with a simple unified API.
   
    * Asset Tracking (Beta)
      
      Track assets in realtime with a solution optimised for last mile
      logistics.
   
   
   INDUSTRY
   
    * EdTech
      
      Deliver interactive learning experiences.
   
    * FinTech
      
      Deliver personalised financial data in realtime.
   
    * Automotive, Logistics, & Mobility
      
      Power diagnostics, order tracking and more.
   
    * B2B Platforms
      
      Empower your customers with realtime solutions.
   
    * Healthcare (HIPAA)
      
      Provide trustworthy, HIPAA-compliant realtime apps.
   
    * eCommerce & Retail
      
      Enrich customer experiences with realtime updates.
   
    * Sports, Media & Audience Engagement
      
      Deliver engaging global realtime experiences.
   
    * Gaming
      
      Power ultra fast and reliable gaming experiences.
   
    * IoT & Connected Devices
      
      Monitor and control global IoT deployments in realtime.
   
   
 * Company Company
   
   
   WHY COMPANIES CHOOSE ABLY
   
    * Customers
      
      Ably supports customers across multiple industries.
   
    * Case studies
      
      Discover how customers are benefiting from Ably.
   
    * Compare our tech
      
      Choose the right realtime service.
   
    * Partners
      
      Ably collaborates and integrates with AWS.
   
    * Resources
      
      Learn more about realtime with our handy resources.
   
    * About Ably
      
      Find out more about Ably’s mission.
   
    * Careers
      
      Discover our open roles and core Ably values.
   
    * Events
      
      Join Ably at upcoming events.
   
   
   BLOG
   
    * How to enable reaction emojis for in-game chat with React
      
      Jan 30, 2024
   
    * Using Presence in in-game chat: Is the other person still there?
      
      Jan 22, 2024
   
    * How to build a live chat widget in React
      
      Jan 16, 2024
   
   More from our Blog
   
 * Developers Developers
   
   
   EXPLORE
   
    * Documentation
      
      Technical guides to help you build with Ably.
   
    * Quickstart guides
      
      Documentation to help you get started quickly.
   
    * Integrations
      
      Find out more about Ably integrations.
   
    * Live examples
      
      Discover our features and their use cases.
   
    * SDKs
      
      Download an SDK to help you build realtime apps faster.
   
    * Tutorials & Demos
      
      Get stuck in with our hands-on resources.
   
    * Chat apps reference guide
      
      Learn how to build chat apps with Ably.
   
    * Multiplayer reference guide
      
      Learn how to build collaborative features with Ably.
   
   
   QUICK LINKS
   
    * Discord
   
    * GitHub
   
    * Changelog
   
    * Status
   
    * Support & FAQs
   
   
 * Pricing

 * Contact us
 * Login
 * 
   Search
   
   Popular pages
   
    * How does Ably work?
    * Quickstart guide
    * Publish/Subscribe Messaging
    * Platform
   
   Support
 * Sign up free

 * Login

 * Popular pages
   
    * How does Ably work?
    * Quickstart guide
    * Publish/Subscribe Messaging
    * Platform
   
    * Products
      Back
      
      
      THE ABLY PLATFORM
      
      Easily power any realtime experience in your application. No complex
      infrastructure to manage or provision. Just a simple API that handles
      everything realtime, and lets you focus on your code.
      
      Explore how it works
      
      
      PRODUCTS
      
       * Pub/Sub Channels
         
         Build infinitely scalable realtime applications.
      
       * Spaces (Beta)
         
         Create multi-user collaborative environments.
      
       * LiveSync (Pre release)
         
         Keep clients in sync with any relational database.
      
      
      TECHNOLOGY
      
       * Predictable performance
         
         A low-latency and high-throughput global network.
      
       * Guaranteed ordering & delivery
         
         Data is delivered - in order - even after disconnections.
      
       * Fault tolerant infrastructure
         
         Redundancy is built in at global and regional levels.
      
       * High scalability & availability
         
         Built for scale with legitimate 99.999% uptime SLAs.
      
       * Global edge network
         
         An edge network of 15 core routing datacenters and 205+ PoPs.
      
      Explore Four Pillars of Dependability
      
    * Solutions
      Back
      
      
      SOLUTIONS
      
       * Live Chat
         
         Deliver highly reliable chat experiences at scale.
      
       * Multiplayer Collaboration
         
         Bring collaborative multiplayer experiences to your users.
      
       * Data Broadcast
         
         Broadcast realtime event data to millions of devices around the globe.
      
       * Data Synchronization
         
         Keep your frontend and backend in realtime sync, at global scale.
      
       * Notifications
         
         Deliver cross-platform push notifications with a simple unified API.
      
       * Asset Tracking (Beta)
         
         Track assets in realtime with a solution optimised for last mile
         logistics.
      
      
      INDUSTRY
      
       * EdTech
         
         Deliver interactive learning experiences.
      
       * FinTech
         
         Deliver personalised financial data in realtime.
      
       * Automotive, Logistics, & Mobility
         
         Power diagnostics, order tracking and more.
      
       * B2B Platforms
         
         Empower your customers with realtime solutions.
      
       * Healthcare (HIPAA)
         
         Provide trustworthy, HIPAA-compliant realtime apps.
      
       * eCommerce & Retail
         
         Enrich customer experiences with realtime updates.
      
       * Sports, Media & Audience Engagement
         
         Deliver engaging global realtime experiences.
      
       * Gaming
         
         Power ultra fast and reliable gaming experiences.
      
       * IoT & Connected Devices
         
         Monitor and control global IoT deployments in realtime.
      
      
    * Company
      Back
      
      --------------------------------------------------------------------------------
      
      
      WHY COMPANIES CHOOSE ABLY
      
       * Customers
         
         Ably supports customers across multiple industries.
      
       * Case studies
         
         Discover how customers are benefiting from Ably.
      
       * Compare our tech
         
         Choose the right realtime service.
      
       * Partners
         
         Ably collaborates and integrates with AWS.
      
       * Resources
         
         Learn more about realtime with our handy resources.
      
       * About Ably
         
         Find out more about Ably’s mission.
      
       * Careers
         
         Discover our open roles and core Ably values.
      
       * Events
         
         Join Ably at upcoming events.
      
      
      BLOG
      
       * How to enable reaction emojis for in-game chat with React
         
         Jan 30, 2024
      
       * Using Presence in in-game chat: Is the other person still there?
         
         Jan 22, 2024
      
       * How to build a live chat widget in React
         
         Jan 16, 2024
      
      More from our Blog
      
    * Developers
      Back
      
      --------------------------------------------------------------------------------
      
      
      EXPLORE
      
       * Documentation
         
         Technical guides to help you build with Ably.
      
       * Quickstart guides
         
         Documentation to help you get started quickly.
      
       * Integrations
         
         Find out more about Ably integrations.
      
       * Live examples
         
         Discover our features and their use cases.
      
       * SDKs
         
         Download an SDK to help you build realtime apps faster.
      
       * Tutorials & Demos
         
         Get stuck in with our hands-on resources.
      
       * Chat apps reference guide
         
         Learn how to build chat apps with Ably.
      
       * Multiplayer reference guide
         
         Learn how to build collaborative features with Ably.
      
      
      QUICK LINKS
      
       * Discord
      
       * GitHub
      
       * Changelog
      
       * Status
      
       * Support & FAQs
      
      
    * Pricing
   
   --------------------------------------------------------------------------------
   
   Contact us Sign up free

Tutorials
Show tutorial in: JavaScript Java Android Kotlin Python PHP Ruby Node.js
TypeScript Obj-C Swift Go C# .NET C++ Flutter C CSS Appcelerator PhoneGap curl
Toggle menu
Mobile

BUILDING A REALTIME CRYPTOCURRENCY APP WITH FLUTTER

Learn how to build a live cryptocurrency app using Ably's Flutter plugin

 * Overview
 * 1. Pre-requisites
 * 2. Building the Realtime Cryptocurrency Charts
 * 3. Building the Flutter Chat Room
 * 4. Viewing Recent Tweets for Each Coin
 * 5. Conclusion and next steps

View as single page

AUTHORS






BUILDING A REALTIME CRYPTOCURRENCY APP WITH FLUTTER

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

Flutter 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 data stream on
    the Ably 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.



The tech world is increasingly moving towards event-driven systems, giving rise
to a need for fast and reactive applications. The 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.

2. Project files:

You can start from scratch and follow along with the tutorial by creating a new
Flutter project in the desired location, and removing all the unnecessary code:

flutter create live-cryptocurrency-streaming-app

or clone the GitHub repo that already has the full project.

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

flutter pub get

4. The next step is to sign up 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. For our application,
we’ll make use of the Cryptocurrency 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 for a Twitter developer account to get your 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 in
the lib folder and paste your keys in it as follows:

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.


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 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.

dependencies:
  flutter:
    sdk: flutter

  ably_flutter: ^1.2.0-preview.1
  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

Here’s an explanation on the packages we’ve added.

 1. ably_flutter
    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
    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
    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 on the dashboard page.
 4. intl
    The most popular internationalization package for Flutter, we will use it
    for dates formatting.
 5. 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
    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
    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 Ably Cryptocurrency prices 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.

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 the service layer.

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.

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

The reason we do this is that we want this service to be a Singleton 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.

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.

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 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 the 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.

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:

1. Get the realtime channel relevant to the data stream we are interested in

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

In a production-level app, it would be a good idea to check if the requested
channel was successfully obtained.

2. Subscribe to that channel

final Stream<ably.Message> messageStream = channel.subscribe();

3. 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.

4. So we register a listener for this message stream, and use the where method
to filter out null values.

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 graphs of the price.

To be able to access our services easily we use the service locator package
get_it. Feel free to use the 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.






2.5. REGISTERING SERVICES WITH GET_IT

The first step is to register the service using the 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.

GetIt getIt = GetIt.instance;

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

Check out the package documentation on GitHub 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 the dashboard.dart file, make a StatelessWidget and paste the following
code inside the build() method.

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.

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.

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

Since it’s a Stream object, we will use a StreamBuilder widget inside the
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.

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();
    }
  },
),

With all the cases handled, we are ready to display the list of charts if the
connection is successful.


2.7. DISPLAYING CHARTS WITH REAL DATA

In the same file, i.e. dashboard.dart, return the following from the build()
method:

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.

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:

@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 the price on the
Y-axis and time on the X-axis.

Here we will use the Syncfusion 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.

The following code displays the price graphs:

@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,
                    )
                  ],
                )
              ],
            ),
    ),
  );
}

You can see the price graphs in the following screenshot:




With that, we finished building the first page, fully functional with realtime
updates from the Ably Cryptocurrency data stream on the Hub.


3. BUILDING THE FLUTTER CHAT ROOM

In the previous section, we subscribed to a channel as a consumer of a public
data stream. As we don’t have publishing 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 new 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 another method to send new
messages.

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). 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.

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 the 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 client ID 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 let’s 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 the chat.dart file, we will initialize the channel and set up our listener:

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.

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.

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 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 and 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:

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:

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:

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.

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 every time 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.

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 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.

 * Understand the client-side considerations when building realtime apps with
   Flutter and WebSockets.

 * Read more on Simple Flutter app state management with ChangeNotifier.

 * Read more on Using Flutter packages.

 * Learn more about Dart Streams in their official YouTube video.

 * If you have any questions, feel free to reach out to us and we’ll be happy to
   help you out.

 * You can follow the roadmap and request new features directly on Ably Flutter
   plugin’s GitHub repo.


Previous Toggle menu 1. Pre-requisites

Talk to our technical team

If you're having technical or account issues just get in touch.

Start a live chat

Don’t want to chat? Get in touch via our contact page

Try our APIs for free

Our free plan includes

 * 6m messages per month
 * 200 peak connections
 * 200 peak channels
 * Loads of features

Start building


THE ABLY PLATFORM

Easily power any realtime experience in your application via a simple API that
handles everything realtime.

 * Pub/sub messaging
 * Push notifications
 * Third-party integrations
 * Multiple protocol messaging


ABLY IS FOR

 * Ably Asset Tracking
 * Extend Kafka to the edge
 * EdTech
 * Automotive, Logistics, & Mobility
 * B2B Platforms
 * Healthcare
 * eCommerce & Retail
 * Sports & Media
 * Gaming
 * IoT & Connected Devices


DEVELOPERS

 * Start in 5 minutes
 * Documentation
 * Tutorials
 * Changelog
 * Support & FAQs
 * SDKs
 * System status


WHY ABLY

 * Customers
 * Case Studies
 * Four Pillars of Dependability
 * Compare our tech
 * Multi protocol support
 * Third-party integrations


ABOUT

 * About Ably
 * Pricing
 * Blog
 * Careers
 * Open protocol policy
 * Press & Media
 * Contact us

--------------------------------------------------------------------------------

We're hiring! Learn more at Glassdoor
We're hiring! Learn more at Glassdoor


--------------------------------------------------------------------------------

Cookies Legals Data Protection Privacy

SOC 2 Type 2

Certified

HIPAA

Compliant

EU GDPR

Certified

256-bit AES

Encryption

Close Dialog






Yes No
Close Dialog

DATA STREAMING TUTORIALS, REALTIME INSIGHTS, AND ABLY ANNOUNCEMENTS STRAIGHT TO
YOUR INBOX

sprite-discord sprite-facebook sprite-github sprite-glassdoor sprite-google
sprite-icon-display-48hrs sprite-icon-display-about-ably-col
sprite-icon-display-api-keys sprite-icon-display-api
sprite-icon-display-asset-tracking-col sprite-icon-display-browser
sprite-icon-display-calendar sprite-icon-display-call-mobile
sprite-icon-display-careers-col sprite-icon-display-case-studies-col
sprite-icon-display-chat-col sprite-icon-display-chat-stack-col
sprite-icon-display-chat-stack sprite-icon-display-cloud-servers
sprite-icon-display-compare-tech-col sprite-icon-display-customers-col
sprite-icon-display-data-broadcast-col
sprite-icon-display-data-synchronization-col sprite-icon-display-docs-col
sprite-icon-display-documentation sprite-icon-display-events-col
sprite-icon-display-examples-col sprite-icon-display-gdpr
sprite-icon-display-general-comms sprite-icon-display-hipaa
sprite-icon-display-integrations-col sprite-icon-display-it-support-access
sprite-icon-display-it-support-helpdesk
sprite-icon-display-kafka-at-the-edge-col sprite-icon-display-laptop
sprite-icon-display-lightbulb-col sprite-icon-display-live-chat
sprite-icon-display-map-pin sprite-icon-display-message
sprite-icon-display-padlock-closed sprite-icon-display-platform
sprite-icon-display-play sprite-icon-display-privacy-shield-framework
sprite-icon-display-push-notifications-col
sprite-icon-display-quickstart-guides-col sprite-icon-display-resources-col
sprite-icon-display-sdks-col sprite-icon-display-servers
sprite-icon-display-shopping-cart sprite-icon-display-sla
sprite-icon-display-soc2-type2 sprite-icon-display-tech-account-comms
sprite-icon-display-tutorials-demos-col sprite-icon-display-virtual-events-col
sprite-icon-display-virtual-events sprite-icon-gui-ably-badge
sprite-icon-gui-arrow-bidirectional-horizontal
sprite-icon-gui-arrow-bidirectional-vertical sprite-icon-gui-arrow-down
sprite-icon-gui-arrow-left sprite-icon-gui-arrow-right sprite-icon-gui-arrow-up
sprite-icon-gui-burger-menu sprite-icon-gui-check-circled-fill-black
sprite-icon-gui-check-circled-fill sprite-icon-gui-check-circled
sprite-icon-gui-checklist-checked sprite-icon-gui-clock sprite-icon-gui-close
sprite-icon-gui-copy sprite-icon-gui-cross-circled-fill
sprite-icon-gui-cross-circled sprite-icon-gui-dash-circled
sprite-icon-gui-disclosure-arrow sprite-icon-gui-document-generic
sprite-icon-gui-enlarge sprite-icon-gui-external-link
sprite-icon-gui-filter-flow-step-1 sprite-icon-gui-filter-flow-step-2
sprite-icon-gui-filter-flow-step-3 sprite-icon-gui-history sprite-icon-gui-info
sprite-icon-gui-link-arrow sprite-icon-gui-link sprite-icon-gui-live-chat
sprite-icon-gui-minus sprite-icon-gui-plus sprite-icon-gui-quote-marks-solid
sprite-icon-gui-refresh sprite-icon-gui-resources sprite-icon-gui-search
sprite-icon-gui-tick sprite-icon-gui-warning
sprite-icon-live-updates-results-metrics-col sprite-icon-multi-user-spaces-col
sprite-icon-social-x sprite-icon-tech-apachekafka sprite-linkedin sprite-quote
sprite-stackoverflow sprite-twitter sprite-youtube