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:
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.
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.
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
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.
-
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
- or clone the github repo that already has the full project.
- Add the packages mentioned in step 1.4. Packages and dependencies to
pubspec.yaml
file and run:
flutter pub get
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.
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.
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
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 = '';
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
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.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.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.intl
The most popular internationalization package for Flutter, we will use it for dates formatting.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.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.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",
},
];
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,
});
}
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 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);
}
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 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:
- 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 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();
The returned type from
subscribe()
is aStream<Message>
, which feels a bit odd because we just subscribed to something. In reality thesubscribe
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) {});
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.
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());
}
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();
},
),
);
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 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();
}
},
),
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),
],
),
),
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 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.
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(15),
padding: EdgeInsets.all(15),
height: 410,
decoration: BoxDecoration(
color: Color(0xffEDEDED).withOpacity(0.05),
borderRadius: BorderRadius.circular(8.0)),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: queue.isEmpty
? Center(
key: UniqueKey(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(
height: 24,
),
Text('Waiting for coin data...')
],
),
)
: Column(
key: ValueKey(coinName),
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FlatButton(
onPressed: () => _navigateToTwitterFeed(coinName),
textColor: Colors.white,
child: Row(
children: [
Image.asset(
'assets/icon_awesome_twitter.png',
height: 20,
),
SizedBox(width: 10),
Text(
"#$coinName",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
],
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
child: Text(
"\$${widget.coinUpdates.coin.price.toStringAsFixed(2)}",
key: ValueKey(widget.coinUpdates.coin.price),
style: TextStyle(
fontSize: 20,
),
),
),
],
),
SizedBox(height: 25),
SfCartesianChart(
enableAxisAnimation: true,
primaryXAxis: DateTimeAxis(
dateFormat: intl.DateFormat.Hms(),
intervalType: DateTimeIntervalType.minutes,
desiredIntervals: 10,
axisLine: AxisLine(width: 2, color: Colors.white),
majorTickLines: MajorTickLines(color: Colors.transparent),
),
primaryYAxis: NumericAxis(
numberFormat: intl.NumberFormat('##,###.00'),
desiredIntervals: 5,
decimalPlaces: 2,
axisLine: AxisLine(width: 2, color: Colors.white),
majorTickLines: MajorTickLines(color: Colors.transparent),
),
plotAreaBorderColor: Colors.white.withOpacity(0.2),
plotAreaBorderWidth: 0.2,
series: <LineSeries<Coin, DateTime>>[
LineSeries<Coin, DateTime>(
animationDuration: 0.0,
width: 2,
color: Theme.of(context).primaryColor,
dataSource: queue.toList(),
xValueMapper: (Coin coin, _) => coin.dateTime,
yValueMapper: (Coin coin, _) => coin.price,
)
],
)
],
),
),
);
}
With that we finished building the first page, fully functional with realtime updates from the Ably Cryptocurrency 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;
}
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 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();
}
},
);
}
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 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.';
});
}
}
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 Ably and they’ll be happy to help you out.
You can follow the roadmap and request new features directly on Ably Flutter plugin’s GitHub repo.
Top comments (0)