Building a Flutter Crypto Trading App Final: Websockets
Tags: flutter, dart, blockchain, mobile
Published: 2022-11-30 18:44:05

<h2><strong> Introduction</strong></h2>
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.

<p align = "center">
<img src= "https://raw.githubusercontent.com/Obikwelu/mediafile/main/blog%20media/chart.jpg" width ="300" height="300"  />

* 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 

<h2><strong> Brief about trading</strong></h2>

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 


***
<h2><strong> Building the app
</strong></h2>

<p align = "center">
<img src= "https://raw.githubusercontent.com/Obikwelu/mediafile/main/blog%20media/other.jpg" width ="600" height="300"  />

<strong><h3> News Feature </h3></strong>
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

```yaml
  web_socket_channel: ^2.2.0
  intl: ^0.17.0
```

* Create our model file name it news.dart.
 ```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

```dart
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

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

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



<strong><h3> Search Feature and Technical indicator </h3></strong>
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 

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

```dart
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"),
                      ),
                    );
                  }
              }
            },
          ),
        ),
      ),
    );
  }
}
```

<p align = "center">
<img src= "https://raw.githubusercontent.com/Obikwelu/mediafile/main/blog%20media/news.gif"   />

<strong><h3> Realtime Data from Binance API via websockets </h3></strong>

We create the dataStream() function for consuming data from the Binance websocket.

```dart
 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

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

<p align = "center">
<img src= "https://raw.githubusercontent.com/Obikwelu/mediafile/main/blog%20media/websocket final.compressed.gif"   />

*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](https://www.codenjobs.com/blog?&title=Building-a-Flutter-Crypto-Trading-App-Websockets:-Final&id=cfc18716-d052-4117-9548-6e4698919a74)

To know more about codenjobs click [CodenJobs]((https://www.codenjobs.com)

```
Want to Connect?

This article is originally published at codenjobs.com .