DEV Community

loading...
Cover image for Building a Realtime Cryptocurrency App with Flutter
Ably Realtime

Building a Realtime Cryptocurrency App with Flutter

prmais profile image Mais Originally published at ably.io ・20 min read

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.

Alt Text

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 the tutorial by creating a new Flutter project in the desired location, and remove all unnecessary code:
flutter create live-cryptocurrency-streaming-app
Enter fullscreen mode Exit fullscreen mode
  1. Add the packages mentioned in step 1.4. Packages and dependencies to pubspec.yaml file and run:
flutter pub get
Enter fullscreen mode Exit fullscreen mode
  1. 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.

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

  3. Next you need to sign up 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
Enter fullscreen mode Exit fullscreen mode

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:

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 = '';
Enter fullscreen mode Exit fullscreen mode

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":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_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
Enter fullscreen mode Exit fullscreen mode
  1. 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
    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 in 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 Cryptocurrency prices hub 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",
  },
];
Enter fullscreen mode Exit fullscreen mode

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.

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

  Coin({
    this.code,
    this.price,
    this.dateTime,
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

The reason we do this is because 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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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.
ably.RealtimeChannel channel = _realtime.channels.get('[product:ably-coindesk/crypto-pricing]$coinCode:usd');
Enter fullscreen mode Exit fullscreen mode

In a real App it’s probably a good idea to check if we got the channel that we requested.

  • Subscribe to that channel
final Stream<ably.Message> messageStream = channel.subscribe();
Enter fullscreen mode Exit fullscreen mode
  • 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.

messageStream.where((event) => event.data != null).listen((message) {});
Enter fullscreen mode Exit fullscreen mode

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

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.

GetIt getIt = GetIt.instance;

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

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 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();
    },
  ),
);
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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.

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();
    }
  },
),
Enter fullscreen mode Exit fullscreen mode

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

SingleChildScrollView(
    child: Column(
        children: [
            for (CoinUpdates update in prices)
   CoinGraphItem(coinUpdates: update),
         ],
    ),
),
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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

@override
void dispose() {
    widget.coinUpdates.removeListener(_listener);
    super.dispose();
}
Enter fullscreen mode Exit fullscreen mode

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

Syncfusion charts used in the Dashboard

@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,
                    )
                  ],
                )
              ],
            ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

With that we finished building the first page, fully functional with realtime updates from the Ably Cryptocurrency 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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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}");
}
Enter fullscreen mode Exit fullscreen mode

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:

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();
      }
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

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,
      );
    },
  ),
),
Enter fullscreen mode Exit fullscreen mode

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,
            ),
          )
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  );
}
Enter fullscreen mode Exit fullscreen mode

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

static const String path = "search/tweets.json";
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

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.';
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)

pic
Editor guide