DEV Community

Cover image for Building a Flutter Crypto Trading App Final: Websockets
Francis
Francis

Posted on • Updated on

Building a Flutter Crypto Trading App Final: Websockets

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

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

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

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

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

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


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

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

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 .

Enter fullscreen mode Exit fullscreen mode

Top comments (0)