WebSocket with Riverpod in Flutter
Nowadays, many applications need real-time data to provide instant feedback to users, be it a chat application that shows a person typing in real-time or a remote application that plots data directly from a hardware sensor.
We try to solve these problems with REST but face a complex problem: to have near-instant feedback, we must ping the server several times per minute, which can be architecturally difficult to achieve and it overloads the server.
What are WebSockets?
WebSockets represent a long-awaited evolution in the client/server web technology. It defines a fully duplex bi-directional communication channel between the client and server.
In simple words, once after the initial handshake where the server and the client agree to upgrade to WebSockets, (from HTTP) the client and the server can talk in real time without having to continuously make requests (like loading the page again and again).
What is Riverpod?
Riverpod is State-management library that is rewrite of Provider that catches programming errors at compile time rather than at runtime, removes nesting for listening/combining objects, ensures that the code is testable.
Check out Riverpod State management in Flutter to get idea about Riverpod.
To get more detail see its documentation: https://riverpod.dev.
Trading Price Tracker App
You can get Source code of following demo from Github.
To use functionality of Websocket and Riverpod we will create a Live Trading Price Tracker Demo from API of https://developers.binary.com/demos. Here you will find all API related to Trading.
We will create a Demo of where thew will be one Dropdown where there is list of display different markets available in Trading, A Second Dropdown where based on the selection of first dropdown, displays the available symbols available for the selected market. And Text shows the current price. The default color is grey, changes red if the price goes down, green when it goes up. Goes back to grey if the price is unchanged.
For above Demo we will utilise following API’s:
- https://developers.binary.com/api/#active_symbols
- https://developers.binary.com/api/#ticks
- https://developers.binary.com/api/#forget
After completing app will have UI some thing like this:
Let’s Get Started
App Structure
Create new Flutter project in you IDE. The structure of App will be as following.
Add Dependency
First we will add WebSocket and Riverpod Dependency into our project through pubspec.yaml
dependencies:
flutter:
sdk: flutter
web_socket_channel: latest_version
flutter_riverpod: latest_version
Add Model Class
Now we will add Model class to handle Websocket response. Create new folder with name model, Inside it model class for Available market by naming model_active_symbol.dart
class ModelActiveSymbol {
List<activesymbols>? activeSymbols;
EchoReq? echoReq;
String? msgType;
ModelActiveSymbol({activeSymbols, echoReq, msgType});
ModelActiveSymbol.fromJson(Map<string, dynamic=""> json) {
if (json['active_symbols'] != null) {
activeSymbols = <activesymbols>[];
json['active_symbols'].forEach((v) {
activeSymbols!.add(ActiveSymbols.fromJson(v));
});
}
echoReq = json['echo_req'] != null
? EchoReq.fromJson(json['echo_req'])
: null;
msgType = json['msg_type'];
}
Map<string, dynamic=""> toJson() {
final Map<string, dynamic=""> data = <string, dynamic="">{};
if (activeSymbols != null) {
data['active_symbols'] =
activeSymbols!.map((v) => v.toJson()).toList();
}
if (echoReq != null) {
data['echo_req'] = echoReq!.toJson();
}
data['msg_type'] = msgType;
return data;
}
}
class ActiveSymbols{
int? allowForwardStarting;
String? displayName;
int? exchangeIsOpen;
int? isTradingSuspended;
String? market;
String? marketDisplayName;
double? pip;
String? submarket;
String? submarketDisplayName;
String? symbol;
String? symbolType;
ActiveSymbols(
{allowForwardStarting,
displayName,
exchangeIsOpen,
isTradingSuspended,
market,
marketDisplayName,
pip,
submarket,
submarketDisplayName,
symbol,
symbolType});
ActiveSymbols.fromJson(Map<string, dynamic=""> json) {
allowForwardStarting = json['allow_forward_starting'];
displayName = json['display_name'];
exchangeIsOpen = json['exchange_is_open'];
isTradingSuspended = json['is_trading_suspended'];
market = json['market'];
marketDisplayName = json['market_display_name'];
pip = json['pip'];
submarket = json['submarket'];
submarketDisplayName = json['submarket_display_name'];
symbol = json['symbol'];
symbolType = json['symbol_type'];
}
Map<string, dynamic=""> toJson() {
final Map<string, dynamic=""> data = <string, dynamic="">{};
data['allow_forward_starting'] = allowForwardStarting;
data['display_name'] = displayName;
data['exchange_is_open'] = exchangeIsOpen;
data['is_trading_suspended'] = isTradingSuspended;
data['market'] = market;
data['market_display_name'] = marketDisplayName;
data['pip'] = pip;
data['submarket'] = submarket;
data['submarket_display_name'] = submarketDisplayName;
data['symbol'] = symbol;
data['symbol_type'] = symbolType;
return data;
}
}
class EchoReq {
String? activeSymbols;
String? productType;
EchoReq({activeSymbols, productType});
EchoReq.fromJson(Map<string, dynamic=""> json) {
activeSymbols = json['active_symbols'];
productType = json['product_type'];
}
Map<string, dynamic=""> toJson() {
final Map<string, dynamic=""> data = <string, dynamic="">{};
data['active_symbols'] = activeSymbols;
data['product_type'] = productType;
return data;
}
}
Add one more model class name model_tick.dart
class ModelTick {
EchoReq? echoReq;
String? msgType;
Subscription? subscription;
Tick? tick;
ModelTick({echoReq, msgType, subscription, tick});
ModelTick.fromJson(Map<String, dynamic> json) {
echoReq = json['echo_req'] != null
? EchoReq.fromJson(json['echo_req'])
: null;
msgType = json['msg_type'];
subscription = json['subscription'] != null
? Subscription.fromJson(json['subscription'])
: null;
tick = json['tick'] != null ? Tick.fromJson(json['tick']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (echoReq != null) {
data['echo_req'] = echoReq!.toJson();
}
data['msg_type'] = msgType;
if (subscription != null) {
data['subscription'] = subscription!.toJson();
}
if (tick != null) {
data['tick'] = tick!.toJson();
}
return data;
}
}
class EchoReq {
int? subscribe;
String? ticks;
EchoReq({subscribe, ticks});
EchoReq.fromJson(Map<String, dynamic> json) {
subscribe = json['subscribe'];
ticks = json['ticks'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['subscribe'] = subscribe;
data['ticks'] = ticks;
return data;
}
}
class Subscription {
String? id;
Subscription({id});
Subscription.fromJson(Map<String, dynamic> json) {
id = json['id'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
return data;
}
}
class Tick {
double? ask;
double? bid;
int? epoch;
String? id;
int? pipSize;
double? quote;
String? symbol;
Tick(
{ask,
bid,
epoch,
id,
pipSize,
quote,
symbol});
Tick.fromJson(Map<String, dynamic> json) {
try{
ask = json['ask'];
}catch(e){
int tempAsk = json['ask'];
ask = tempAsk.toDouble();
}
bid = json['bid'].toDouble();
epoch = json['epoch'];
id = json['id'];
pipSize = json['pip_size'];
quote = getDouble(json['quote']);
symbol = json['symbol'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['ask'] = ask;
data['bid'] = bid;
data['epoch'] = epoch;
data['id'] = id;
data['pip_size'] = pipSize;
data['quote'] = quote;
data['symbol'] = symbol;
return data;
}
double getDouble(dynamic data){
try{
double value = data;
return value;
}catch(e){
int tempValue = data;
return tempValue.toDouble();
}
}
}
Add WebScoket class
Create new folder inside lib and name it web_socket, Inside it create new dart file name web_socker.dart
and we will create Single ton WebSocket class by adding following code:
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocket{
static const String CONNECT_URL = "wss://ws.binaryws.com/websockets/v3?app_id=1089";
static late WebSocketChannel channel;
static final WebSocket _webSocket = WebSocket._internal();
static late Function(dynamic event) listener;
factory WebSocket(Function(dynamic event) listen) {
listener = listen;
return _webSocket;
}
WebSocket._internal(){
channel = WebSocketChannel.connect(
Uri.parse(CONNECT_URL),
);
///Receive data of send request in stream listener
channel.stream.listen(listener);
}
///send request
void sendRequest(Map<String,dynamic> request){
channel.sink.add(jsonEncode(request));
}
}
Add Controller
Now to separate logic from UI we will create notifier folder and inside it we will create controller for Available market in trading and controller for market price update. With the help of Riverpod this controllers state is going to be managed.
Now create file name market_notifier.dar
t and add following logic inside it:
final socketProvider = ChangeNotifierProvider<MarketProvider>((ref) {
return MarketProvider(ref);
});
class MarketProvider extends ChangeNotifier{
late final WebSocket webSocket = WebSocket(setUpListener());
late ModelActiveSymbol modelActiveSymbol;
late ModelTick modelTick;
Map<String,List<ActiveSymbols>> uniqueActiveSymbolByMarketName = <String,List<ActiveSymbols>>{};
late String selectedMarket;
late ActiveSymbols selectedSymbol;
double defaultPrice =-1;
bool isSymbolChanged = true;
final Ref ref;
MarketProvider(this.ref){
getMarketPlace();
}
///Get initial market places
getMarketPlace(){
Map<String,String> activeSymbolRequest ={
"active_symbols": "brief",
"product_type": "basic"
};
webSocket.sendRequest(activeSymbolRequest);
}
///Listener of web socket called at every event
Function(dynamic event) setUpListener(){
return ((event) {
Map<String, dynamic> response = jsonDecode(event);
/// Get all Market
if (response["msg_type"] == "active_symbols") {
modelActiveSymbol = ModelActiveSymbol.fromJson(response);
for (var element in modelActiveSymbol.activeSymbols!) {
if (uniqueActiveSymbolByMarketName.containsKey(element.marketDisplayName)) {
uniqueActiveSymbolByMarketName[element.marketDisplayName]?.add(element);
}
else {
uniqueActiveSymbolByMarketName.putIfAbsent(element.marketDisplayName!, () => [element]);
}
}
selectedMarket = uniqueActiveSymbolByMarketName.keys.first;
selectedSymbol = uniqueActiveSymbolByMarketName[selectedMarket]![0];
notifyListeners();
getTickData(selectedSymbol);
}
/// Get Price base on selected market symbol
if (response["msg_type"] == "tick") {
modelTick = ModelTick.fromJson(response);
if(defaultPrice==-1 || isSymbolChanged){
defaultPrice = modelTick.tick?.quote??-1;
isSymbolChanged = false;
}
ref.read(tickProvider.state).state = AsyncData(modelTick);
}
}
);
}
///Update selected value from drop-down
updateValue<T>(T item){
if(T == String){
selectedMarket =(item as String);
selectedSymbol = uniqueActiveSymbolByMarketName[selectedMarket]![0];
}
else{
selectedSymbol = (item as ActiveSymbols);
}
isSymbolChanged = true;
forgetOldTick();
getTickData(selectedSymbol);
notifyListeners();
}
///Get price from web-socket of selected symbol
getTickData(ActiveSymbols activeSymbols){
Map<String,String> ticksRequest ={
"ticks": activeSymbols.symbol!,
"subscribe": "1"
};
webSocket.sendRequest(ticksRequest);
}
///Remove last symbol price before getting new symbol price
forgetOldTick(){
Map<String,String> forgetTicksRequest ={
"forget": modelTick.tick?.id??"",
};
webSocket.sendRequest(forgetTicksRequest);
}
}
Also controller for market price name trade_notifier.dart
and add following logic inside it:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/model_tick.dart';
final tickProvider = StateProvider<asyncvalue<modeltick>>((ref) {
return const AsyncValue.loading();
});</asyncvalue<modeltick>
Add UI screen
As you can see in folder structure there is a view folder inside which there is re-usable widgets which is generic dropdown and market price.
Create file dropdown.dart
and add following UI code:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/model_active_symbol.dart';
import '../../notifier/market_notifier.dart';
class DropDown<T> extends ConsumerStatefulWidget {
const DropDown({Key? key}) : super(key: key);
@override
ConsumerState<DropDown<T>> createState() => _DropDownState();
}
class _DropDownState<T> extends ConsumerState<DropDown<T>> {
late T dropDownValue;
List<T> items = [];
@override
Widget build(BuildContext context) {
var socketNotifier = ref.watch(socketProvider);
var activeSymbolsByMarketName = socketNotifier.uniqueActiveSymbolByMarketName;
Iterable<String>? marketNames = activeSymbolsByMarketName.keys;
///Market places
if(T == String) {
if (marketNames.isNotEmpty) {
items = (marketNames.toList() as List<T>);
dropDownValue = (socketNotifier.selectedMarket as T);
}
///Symbols as per selected market
}else{
if (marketNames.isNotEmpty && activeSymbolsByMarketName.isNotEmpty) {
items = (activeSymbolsByMarketName[socketNotifier.selectedMarket] as List<T>);
dropDownValue = (socketNotifier.selectedSymbol as T);
}
}
///Generic dropdown
return items.isNotEmpty? InputDecorator(
decoration: const InputDecoration(border: OutlineInputBorder()),
child: DropdownButtonHideUnderline(
child: DropdownButton<T>(
value: dropDownValue,
icon: const Icon(Icons.keyboard_arrow_down),
items: items.map((T item) {
return DropdownMenuItem<T>(
value: item,
child: T == String ? Text(item as String): Text((item as ActiveSymbols).symbol??""),
);
}).toList(),
onChanged: (T? newValue) {
socketNotifier.updateValue<T>(newValue as T);
},
),
),
):Container();
}
}
Now text widget to display Live market price which will change colour depending on increase or decrease of market price, Add file market_price
.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../notifier/market_notifier.dart';
import '../../notifier/trade_notifier.dart';
class MarketPriceText extends ConsumerWidget {
const MarketPriceText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var tickNotifier = ref.watch(tickProvider);
var defaultValue = ref.read(socketProvider).defaultPrice;
/// Price displaying widget
return tickNotifier.maybeWhen(
data:(data){
var currentValue = data.tick!.quote!;
///Set color base on value
Color color = currentValue > defaultValue? Colors.green : currentValue < defaultValue ? Colors.red : Colors.grey;
return Text(data.tick?.quote.toString()??"",
style: TextStyle(fontSize: 20,color:color ),);
},
orElse: () => const Center(
child: Text("Loading...."),
),);
}
}
Now we Create a screen where those above widget is call and overall price tracking is done which is screen_price_tracker.dart
import 'package:flutter/material.dart';
import 'package:trading_price_tracker/view/widget/dropdrown.dart';
import 'package:trading_price_tracker/view/widget/market_price.dart';
import '../models/model_active_symbol.dart';
class ScreenTradingPrices extends StatelessWidget {
const ScreenTradingPrices({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15,vertical: 15),
child: Column(
children: const [
SizedBox(height: 20,),
///Market places
DropDown<String>(),
SizedBox(height: 20,),
///Active symbols according to market
DropDown<ActiveSymbols>(),
SizedBox(height: 55,),
/// Live Price of symbols
MarketPriceText()
],
),
),
)
);
}
}
Lastly make change in main.dart
file to call all above changes and launch app
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:trading_price_tracker/view/screen_price_tracker.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Trading price Tracker',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const ScreenTradingPrices(),
);
}
}
That’s It
Now you can launch the app and see the Real-Time price tracking of any market selected from Dropdown. This is only possible through WebSocket like service. After launching app it is going to be look like this.
Where can you use it?
The realtime web existed before WebSockets, but it was difficult to accomplish, often slower, and based on hacking web technologies that weren’t built for realtime applications. The WebSocket protocol paved the path for a genuinely real-time web and expanded the possibilities of Internet communication.
USE CASES:
- Web-based games
- Chatting Applications
- Trading Applications