Building a Flutter Crypto Trading App Final: Websockets
Francis
Posted on November 30, 2022
Introduction
This article is a continuation of my previous article on building a crypto trading app. In this tutorial, we explored more features of syncfusion and took a dive into websockets in flutter.
For this article, I added a news service into the app for getting crypto related news headline
I also enabled searching for any cryptocurrency and view the chart data.
I added technical indicators from the syncfusion package.
finally I used web sockets to obtain real time data from the Binance websocket.
Note: This article is for instructional purpose only. No state management package was used for this tutorial. To follow along use the previous code we created
Brief about trading
In order to trade effectively, whether stocks, forex or crypto, most traders apply different techniques which could be
- Fundamental analysis which includes news (high impact news) that can determine direction of market
- Technical Analysis which are various analytical, statistical or mathematical principles translated into tools which could be used to determine direction of market. Such indicator includes MACD, EMA, Bollinger band, RSI etc. Syncfusion already provides us with some of the technical indicators we require. So we will write a function to make any indicator we want visible
Building the app
News Feature
We will be using the REST API from newsapi.org for this purpose. This because it curates cryptocurrency and blockchain related news from various sources. For this app we will be using only the headlines from the API but you can use other data provided by the API.
We will be using these extra packages
web_socket_channel: ^2.2.0
intl: ^0.17.0
- Create our model file name it news.dart.
import 'dart:convert';
CryptoNews cryptoNewsFromJson(String str) =>
CryptoNews.fromJson(json.decode(str));
class CryptoNews {
CryptoNews({
this.status,
this.totalResults,
this.articles,
});
String? status;
int? totalResults;
List<Article>? articles;
factory CryptoNews.fromJson(Map<String, dynamic> json) => CryptoNews(
status: json["status"],
totalResults: json["totalResults"],
articles: List<Article>.from(
json["articles"].map((x) => Article.fromJson(x))),
);
}
class Article {
Article({
this.source,
this.author,
this.title,
this.description,
this.url,
this.urlToImage,
// this.publishedAt,
this.content,
});
Source? source;
String? author;
String? title;
String? description;
String? url;
String? urlToImage;
// DateTime? publishedAt;
String? content;
factory Article.fromJson(Map<String, dynamic> json) => Article(
source: Source.fromJson(json["source"]),
author: json["author"] == null ? null : json["author"],
title: json["title"],
description: json["description"],
url: json["url"],
urlToImage: json["urlToImage"] == null ? null : json["urlToImage"],
// publishedAt: DateTime.parse(json["publishedAt"]),
content: json["content"],
);
}
class Source {
Source({
this.id,
this.name,
});
String? id;
String? name;
factory Source.fromJson(Map<String, dynamic> json) => Source(
id: json["id"] == null ? null : json["id"],
name: json["name"],
);
}
Then we write the function that will help us consume the data from the API
fetchNews() async {
var datef = DateFormat.yMMMd();
var date = datef.format(DateTime.now());
String url =
"https://newsapi.org/v2/everything?q=crypto&from=$date&sortBy=publishedAt&apiKey=44fff84f274244c9854593b49df128a3";
Uri uri = Uri.parse(url);
final response = await http.get(uri);
final decodedRes = await json.decode(response.body);
return CryptoNews.fromJson(decodedRes);
}
Then create a custom card (newscard.dart) widget for displaying the news headline
import 'package:cnj_charts/models/news.dart';
import 'package:flutter/material.dart';
class NewsCard extends StatelessWidget {
Source? source;
String? author;
String? title;
String? description;
String? url;
String? urlToImage;
String? content;
NewsCard({
Key? key,
this.source,
this.author,
this.title,
this.description,
this.url,
this.urlToImage,
this.content,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 3,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(15.0),
child: Container(
height: 60,
width: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(
urlToImage.toString(),
),
),
),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
title.toString(),
style: const TextStyle(fontSize: 15),
),
],
),
),
],
),
],
),
),
],
),
);
}
}
Then create the newspage.dart screen for displaying our news headline.
import 'package:cnj_charts/models/news.dart';
import 'package:cnj_charts/services/api.dart';
import 'package:cnj_charts/widget/drawer.dart';
import 'package:cnj_charts/widget/newscard.dart';
import 'package:flutter/material.dart';
class NewsPage extends StatefulWidget {
const NewsPage({super.key});
@override
State<NewsPage> createState() => _NewsPageState();
}
class _NewsPageState extends State<NewsPage> {
@override
void initState() {
fetchNews();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: const CryptoDrawer(),
appBar: AppBar(
centerTitle: true,
title: const Text('Crypto News'),
backgroundColor: Colors.black,
),
body: Container(
color: Colors.black,
child: FutureBuilder(
future: fetchNews(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
);
default:
if (snapshot.hasError) {
return Center(
child: ElevatedButton(
onPressed: () {
snapshot.hasData;
},
child: const Text("No network connection")),
);
} else {
final news = snapshot.data as CryptoNews;
return ListView.builder(
itemCount: news.articles!.length,
itemBuilder: (BuildContext context, int index) {
var snap = news.articles![index];
return NewsCard(
urlToImage: snap.urlToImage,
title: snap.title,
);
},
);
}
}
},
),
));
}
}
Search Feature and Technical indicator
In our drawer we create a search box that would enable us search for different coins. In the ListView add a Textfield Widget. Then pass in the controller and the onsubmitted function which basically
TextField(
onSubmitted: (value) {
wickList = [];
cryptoList = [];
Navigator.push(
context,
MaterialPageRoute(
builder: (c) => SearchResult(
cryptocurrency: _controller.text.trim(),
),
),
);
},
controller: _controller,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: "Enter coin name",
fillColor: Colors.white,
filled: true,
),
),
Then in the search result page, we create our trading chart view using the syncfusion SFCartesianChart widget which is similar to the code we wrote in the previous tutorial. Then we also enabled the technicla indicators bollinger band, RSI (Relative Strength Index) and the Simple Moving Average (SMA) indicator. Intl package is also used here to format the date.
Also we add a ListView.Builder which returns a detail conainer. The detail container widget displays real time information about the cryptocurrency via websockets.
import 'package:cnj_charts/models/candles.dart';
import 'package:cnj_charts/models/details.dart';
import 'package:cnj_charts/services/api.dart';
import 'package:cnj_charts/widget/detail_container.dart';
import 'package:cnj_charts/widget/drawer.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:http/http.dart' as http;
class SearchResult extends StatefulWidget {
String cryptocurrency;
SearchResult({Key? key, required this.cryptocurrency}) : super(key: key);
@override
State<SearchResult> createState() => _SearchResultState();
}
class _SearchResultState extends State<SearchResult> {
bool bolisVisible = false;
bool rsiisVisible = false;
bool smaisVisible = false;
late TrackballBehavior _trackballBehavior;
late ZoomPanBehavior _zoomPanBehavior;
late ChartSeriesController _chartSeriesController;
trackerData() async {
String apiEndpoint =
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${widget.cryptocurrency}";
var url = Uri.parse(apiEndpoint);
var response = await http.get(url);
if (response.statusCode == 200) {
var res = response.body;
cryptoList = cryptoTrackerFromJson(res);
return cryptoList;
} else {
return null;
}
}
@override
void initState() {
trackerData();
fetchData(widget.cryptocurrency);
_trackballBehavior = TrackballBehavior(
enable: true,
activationMode: ActivationMode.doubleTap,
);
_zoomPanBehavior = ZoomPanBehavior(
enablePinching: true,
enableDoubleTapZooming: true,
enableSelectionZooming: true,
zoomMode: ZoomMode.x,
enablePanning: true,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
drawer: const CryptoDrawer(),
appBar: AppBar(
centerTitle: true,
title: const Text('CodenJobs charts'),
backgroundColor: Colors.black,
),
body: Container(
color: Colors.black,
child: FutureBuilder(
future: fetchData(widget.cryptocurrency),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return SizedBox(
height: MediaQuery.of(context).size.height,
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
);
default:
if (snapshot.hasData) {
return Column(
mainAxisSize: MainAxisSize.max,
children: [
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: cryptoList.length,
itemBuilder: (context, index) {
if (cryptoList.isNotEmpty) {
var snap = cryptoList[index];
// return Card();
return DetailContainer(
cryptocurrency: widget.cryptocurrency,
symbol: snap.symbol,
snap: snap,
);
} else {
return const Center(
child: Text('Check your network'),
);
}
},
),
Expanded(
child: SfCartesianChart(
// on: ,
enableAxisAnimation: true,
plotAreaBorderWidth: 1,
zoomPanBehavior: _zoomPanBehavior,
title: ChartTitle(
text:
'${widget.cryptocurrency.toUpperCase()}/USDT',
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
trackballBehavior: _trackballBehavior,
indicators: <TechnicalIndicators<dynamic, dynamic>>[
BollingerBandIndicator<dynamic, dynamic>(
seriesName: 'Charts',
isVisible: bolisVisible),
RsiIndicator<dynamic, dynamic>(
seriesName: 'Charts',
isVisible: rsiisVisible,
),
SmaIndicator<dynamic, dynamic>(
seriesName: 'Charts',
valueField: "close",
isVisible: smaisVisible,
)
],
series: <CartesianSeries>[
CandleSeries<CandleItem, DateTime>(
name: "Charts",
dataSource: wickList,
xValueMapper: (CandleItem wick, _) {
int x = int.parse(wick.time.toString());
DateTime time =
DateTime.fromMillisecondsSinceEpoch(x);
return time;
},
lowValueMapper: (CandleItem wick, _) =>
wick.low,
highValueMapper: (CandleItem wick, _) =>
wick.high,
openValueMapper: (CandleItem wick, _) =>
wick.open,
closeValueMapper: (CandleItem wick, _) =>
wick.close,
)
],
primaryXAxis: DateTimeAxis(
dateFormat: DateFormat.Md().add_Hm(),
visibleMinimum:
DateTime.fromMillisecondsSinceEpoch(
int.parse(wickList[0].time.toString())),
visibleMaximum:
DateTime.fromMillisecondsSinceEpoch(
int.parse(wickList[wickList.length - 1]
.time
.toString())),
intervalType: DateTimeIntervalType.auto,
minorGridLines: const MinorGridLines(width: 0),
majorGridLines: const MajorGridLines(width: 0),
edgeLabelPlacement: EdgeLabelPlacement.shift),
primaryYAxis: NumericAxis(
opposedPosition: true,
majorGridLines: const MajorGridLines(width: 0),
enableAutoIntervalOnZooming: true,
),
),
),
const Text("Indicators"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
style:
ElevatedButton.styleFrom(primary: Colors.red),
onPressed: () {
setState(() {
bolisVisible = !bolisVisible;
});
},
child: const Text("Bollinger..."),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.green),
onPressed: () {
setState(() {
rsiisVisible = !rsiisVisible;
});
},
child: const Text("RSI (Relati.."),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.grey,
),
onPressed: () {
setState(() {
smaisVisible = !smaisVisible;
});
},
child: const Text("SMA (Simple..."),
),
],
),
],
);
} else {
return Center(
child: ElevatedButton(
onPressed: () {
setState(() {
snapshot.hasData;
});
},
child: const Text(
"Oops!!! no internet connection or data not found"),
),
);
}
}
},
),
),
),
);
}
}
Realtime Data from Binance API via websockets
We create the dataStream() function for consuming data from the Binance websocket.
dataStream() {
final channel = IOWebSocketChannel.connect(
"wss://stream.binance.com:9443/ws/${widget.symbol}usdt@ticker",
);
channel.stream.listen((message) {
var getData = jsonDecode(message);
setState(() {
btcUsdtPrice = getData['c'];
percent = getData['P'];
volume = getData['v'];
high = getData['h'];
low = getData['l'];
});
});
}
We initialize the datastream() function in the initState.
We then pass in thte specific data we require in the UI
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:web_socket_channel/io.dart';
import 'package:intl/intl.dart' as intl;
class DetailContainer extends StatefulWidget {
String cryptocurrency;
String symbol;
var snap;
DetailContainer({
Key? key,
required this.cryptocurrency,
required this.symbol,
this.snap,
}) : super(key: key);
@override
State<DetailContainer> createState() => _DetailContainerState();
}
class _DetailContainerState extends State<DetailContainer> {
String btcUsdtPrice = "0";
String quantity = "0";
String percent = "0";
String volume = "0";
String high = "0";
String low = "0";
@override
void initState() {
super.initState();
dataStream();
}
dataStream() {
final channel = IOWebSocketChannel.connect(
"wss://stream.binance.com:9443/ws/${widget.symbol}usdt@ticker",
);
channel.stream.listen((message) {
var getData = jsonDecode(message);
setState(() {
btcUsdtPrice = getData['c'];
percent = getData['P'];
volume = getData['v'];
high = getData['h'];
low = getData['l'];
});
});
}
@override
void dispose() {
dataStream();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Card(
color: Colors.grey[800],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(20),
//
),
height: 60,
width: 60,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Image.network(widget.snap.image),
),
),
),
Column(
children: [
const Text(
"Price",
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
Text(
"\$${intl.NumberFormat.decimalPattern().format(double.parse(btcUsdtPrice))}",
style: const TextStyle(
fontSize: 20, color: Colors.white),
),
],
)
],
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
const Text(
"Volume:",
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
Text(
intl.NumberFormat.decimalPattern()
.format(double.parse(volume)),
style: const TextStyle(
fontSize: 20, color: Colors.white),
),
],
),
Column(
children: [
const Text(
"24hr high",
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
Text(
"\$${intl.NumberFormat.decimalPattern().format(double.parse(high))}",
style: const TextStyle(
fontSize: 20, color: Colors.white),
),
],
),
],
),
const SizedBox(
height: 4,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
const Text(
"Change percent",
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
Text(
"$percent%",
// widget.snap.marketCap.toString(),
style: const TextStyle(
fontSize: 20, color: Colors.white),
),
],
),
Column(
children: [
const Text(
"24hr low",
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
Text(
"\$${intl.NumberFormat.decimalPattern().format(double.parse(low))}",
style: const TextStyle(
fontSize: 20, color: Colors.white),
),
],
),
],
),
],
),
],
),
),
);
}
}
Voila we have our crypto trading app ready.
Final note: I addressed most of the issues we noted in part 1 of this series though I did not use any state management package. To make the app better you can use any state management of your choice to ensure that only the necesary part of your app are rebuilt especially when applying the technical indicator. Also, the trading chart can be made realtime using the Binance websocket API that returns candle data for different time frames. The syncfusion package we used has a method for handling realtime data. Since I discovered the Binance websocket API during the part 2 of this series I had to use it only for returning real time price, volume, change percent and 24hrs high and low
Download code: visit Codenjobs.com
Link to original article at CodenJobs
To know more about codenjobs click [CodenJobs]((https://www.codenjobs.com)
Want to Connect?
This article is originally published at codenjobs.com .
Posted on November 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.