0% found this document useful (0 votes)
22 views54 pages

Crypto Tracker Flutter App - !

The document outlines the structure and functionality of a cryptocurrency tracking Flutter app, including themes, API interactions, data models, and UI components. It features a light and dark theme, API requests to fetch market data, and models for cryptocurrencies and graph points. Additionally, it includes a detail page for displaying cryptocurrency information and charts based on user-selected timeframes.

Uploaded by

tabsaummuniba
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
22 views54 pages

Crypto Tracker Flutter App - !

The document outlines the structure and functionality of a cryptocurrency tracking Flutter app, including themes, API interactions, data models, and UI components. It features a light and dark theme, API requests to fetch market data, and models for cryptocurrencies and graph points. Additionally, it includes a detail page for displaying cryptocurrency information and charts based on user-selected timeframes.

Uploaded by

tabsaummuniba
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 54

Crypto tracker flutter app

Lib/constants/Themes.dart
import 'package:flutter/material.dart';

ThemeData lightTheme = ThemeData(


brightness: Brightness.light,
scaffoldBackgroundColor: Colors.white,
appBarTheme: const AppBarTheme(
elevation: 0,
backgroundColor: Color.fromARGB(255, 119, 99, 0),
iconTheme: IconThemeData(
color: Colors.black,
)
)
);

ThemeData darkTheme = ThemeData(


brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xff15161a),
appBarTheme: const AppBarTheme(elevation: 0,
backgroundColor: Color(0xff15161a),)

);

Lib/models/API.dart
// lib/models/API.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

class API {
static const String baseUrl = "https://api.coingecko.com/api/v3";
static const int maxRetries = 3;
static const Duration initialRetryDelay = Duration(seconds: 2);
static const Duration timeoutDuration = Duration(seconds: 15);
static const Map<String, String> headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};

static Future<List<dynamic>> getMarkets() async {


return _fetchWithRetry(
() async {
final requestPath = Uri.parse(
"$baseUrl/coins/markets?
vs_currency=inr&order=market_cap_desc&per_page=100&page=1&sparkline=false"
);
final data = await _makeRequest(requestPath, "markets");
return data as List;
}
);
}

static Future<List<dynamic>> fetchGraphData(String id, int days) async {


return _fetchWithRetry(
() async {
final requestPath = Uri.parse(
"$baseUrl/coins/$id/market_chart?vs_currency=inr&days=$days"
);
final data = await _makeRequest(requestPath, "graph data");
return (data["prices"] as List?) ?? [];
}
);
}

static Future<List<dynamic>> fetchCandlestickData(String id, int days) async {


return _fetchWithRetry(
() async {
final requestPath = Uri.parse(
"$baseUrl/coins/$id/ohlc?vs_currency=inr&days=$days"
);
final data = await _makeRequest(requestPath, "candlestick data");

if (data is! List || data.isEmpty) {


throw FormatException("Invalid OHLC data format");
}
for (final candle in data) {
if (candle is! List || candle.length != 5) {
throw FormatException("Each candle must have 5 elements");
}
}
return data;
}
);
}

static Future<dynamic> _makeRequest(Uri requestPath, String requestType) async {


debugPrint("Fetching $requestType from ${requestPath.path}");

final connectivity = await Connectivity().checkConnectivity();


if (connectivity == ConnectivityResult.none) {
throw Exception("No internet connection");
}

final response = await http.get(requestPath, headers: headers)


.timeout(timeoutDuration);

if (response.statusCode == 200) {
return _parseJson(response.body);
} else if (response.statusCode == 429) {
throw Exception("API rate limit exceeded");
} else {
throw http.ClientException(
"HTTP ${response.statusCode}: ${response.body}",
requestPath
);
}
}

static Future<List<dynamic>> _fetchWithRetry(


Future<List<dynamic>> Function() fetchFn,
) async {
int attempt = 1;
while (attempt <= maxRetries) {
try {
final result = await fetchFn();
if (result.isNotEmpty) return result;
throw Exception("Empty data received");
} catch (e) {
debugPrint("Attempt $attempt failed: $e");
if (attempt == maxRetries) rethrow;

// Exponential backoff
await Future.delayed(initialRetryDelay * pow(1.5, attempt - 1) as Duration);
attempt++;
}
}
return [];
}

static dynamic _parseJson(String responseBody) {


try {
return jsonDecode(responseBody);
} catch (e) {
debugPrint("JSON parse error: $e\nResponse: $responseBody");
throw FormatException("Invalid JSON: ${e.toString()}");
}
}
}

Lib/models/Cryptocurrency.dart
// lib/models/Cryptocurrency.dart
class CryptoCurrency {
String? id;
String? symbol;
String? name;
String? image;
double? currentPrice;
double? marketCap;
int? marketCapRank;
double? high24;
double? low24;
double? priceChange24;
double? priceChangePercentage24;
double? circulatingSupply;
double? ath;
double? atl;
double? totalVolume;
bool isFavorite = false;

// Add getter for priceChange24h to match DetailPage usage


double? get priceChange24h => priceChange24;

CryptoCurrency({
required this.id,
required this.symbol,
required this.name,
required this.image,
required this.currentPrice,
required this.marketCap,
required this.marketCapRank,
required this.high24,
required this.low24,
required this.priceChange24,
required this.priceChangePercentage24,
required this.circulatingSupply,
required this.ath,
required this.atl,
this.totalVolume,
});

factory CryptoCurrency.fromJSON(Map<String, dynamic> map) {


return CryptoCurrency(
id: map["id"],
symbol: map["symbol"],
name: map["name"],
image: map["image"],
currentPrice: double.tryParse(map["current_price"].toString()),
marketCap: double.tryParse(map["market_cap"].toString()),
marketCapRank: map["market_cap_rank"],
high24: double.tryParse(map["high_24h"].toString()),
low24: double.tryParse(map["low_24h"].toString()),
priceChange24: double.tryParse(map["price_change_24h"].toString()),
priceChangePercentage24:
double.tryParse(map["price_change_percentage_24h"].toString()),
circulatingSupply: double.tryParse(map["circulating_supply"].toString()),
ath: double.tryParse(map["ath"].toString()),
atl: double.tryParse(map["atl"].toString()),
totalVolume: double.tryParse(map["total_volume"].toString()),
);
}
}

Lib/models/GraphPoint.dart
// lib/models/GraphPoint.dart
class GraphPoint {
final DateTime date;
final double price;
final double? open;
final double? high;
final double? low;
final double? close;

GraphPoint({
required this.date,
required this.price,
this.open,
this.high,
this.low,
this.close,
});

factory GraphPoint.fromList(List<dynamic> list) {


if (list.length < 2) {
throw FormatException("Price data requires at least [timestamp, price]");
}

return GraphPoint(
date: DateTime.fromMillisecondsSinceEpoch(list[0] as int),
price: (list[1] as num).toDouble(),
);
}

factory GraphPoint.fromOHLC(List<dynamic> list) {


if (list.length != 5) {
throw FormatException("OHLC data requires [timestamp, open, high, low, close]");
}

return GraphPoint(
date: DateTime.fromMillisecondsSinceEpoch(list[0] as int),
open: (list[1] as num).toDouble(),
high: (list[2] as num).toDouble(),
low: (list[3] as num).toDouble(),
close: (list[4] as num).toDouble(),
price: (list[4] as num).toDouble(), // Default price to close
);
}

@override
String toString() {
return 'GraphPoint{date: $date, price: $price, OHLC: $open/$high/$low/$close}';
}
}

Lib/models/LocalStorage.dart
import 'package:shared_preferences/shared_preferences.dart';

class LocalStorage {
static Future<bool> saveTheme(String theme) async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
bool result = await sharedPreferences.setString("theme", theme);
return result;
}

static Future<String?> getTheme() async {


SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
String? currentTheme = sharedPreferences.getString("theme");
return currentTheme;
}

static Future<bool> addFavorite(String id) async {


SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
List<String> favorites = sharedPreferences.getStringList("favorites") ?? [];
favorites.add(id);
return await sharedPreferences.setStringList("favorites", favorites);
}

static Future<bool> removeFavorite(String id) async {


SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
List<String> favorites = sharedPreferences.getStringList("favorites") ?? [];
favorites.remove(id);
return await sharedPreferences.setStringList("favorites", favorites);
}

static Future<List<String>> fetchFavorites() async {


SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
return sharedPreferences.getStringList("favorites") ?? [];
}
}

Lib/models/user_model.dart
class UserModel {
String? uid;
String? email;
String? firstName;
String? secondName;

UserModel({this.uid, this.email, this.firstName, this.secondName});


// receiving data from server
factory UserModel.fromMap(map) {
return UserModel(
uid: map['uid'],
email: map['email'],
firstName: map['firstName'],
secondName: map['secondName'],
);
}

// sending data to our server


Map<String, dynamic> toMap() {
return {
'uid': uid,
'email': email,
'firstName': firstName,
'secondName': secondName,
};
}
}

Lib/pages/DetailPage.dart
// lib/pages/DetailPage.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:crytoapp/models/GraphPoint.dart';
import 'package:crytoapp/models/Cryptocurrency.dart';
import 'package:crytoapp/providers/market_provider.dart';
import 'package:crytoapp/providers/graph_provider.dart';
import 'package:fl_chart/fl_chart.dart';
import 'dart:math';

class DetailsPage extends StatefulWidget {


final String id;

const DetailsPage({Key? key, required this.id}) : super(key: key);

@override
_DetailsPageState createState() => _DetailsPageState();
}

class _DetailsPageState extends State<DetailsPage> {


int days = 1;
final List<bool> isSelected = [true, false, false, false, false];

@override
void initState() {
super.initState();
_loadData();
}

void _loadData() {
final graphProvider = Provider.of<GraphProvider>(context, listen: false);
graphProvider.initializeGraph(widget.id, days);
}

void _handleTimeframeChange(int index) {


setState(() {
days = [1, 7, 28, 90, 365][index];
for (int i = 0; i < isSelected.length; i++) {
isSelected[i] = i == index;
}
});
_loadData();
}

Widget _buildChart(BuildContext context, GraphProvider graphProvider) {


if (graphProvider.isLoading(widget.id)) {
return _buildLoadingWidget();
}

if (graphProvider.getErrorMessage(widget.id) != null) {
return _buildErrorWidget(graphProvider.getErrorMessage(widget.id)!);
}

final points = graphProvider.showCandlestick


? graphProvider.getCandlestickPoints(widget.id)
: graphProvider.getGraphPoints(widget.id);

if (points.isEmpty) {
return _buildErrorWidget("No data available for selected period");
}

return graphProvider.showCandlestick
? _buildCandlestickChart(points)
: _buildLineChart(points);
}

Widget _buildLineChart(List<GraphPoint> points) {


final validPoints = points.where((p) => p.price != null).toList();
final spots = validPoints
.asMap()
.map((i, p) => MapEntry(i, FlSpot(i.toDouble(), p.price!)))
.values
.toList();

return LineChart(
LineChartData(
minX: 0,
maxX: spots.length.toDouble(),
minY: spots.map((s) => s.y).reduce(min) * 0.99,
maxY: spots.map((s) => s.y).reduce(max) * 1.01,
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: Colors.cyan,
barWidth: 2,
belowBarData: BarAreaData(show: true, color:
Colors.cyan.withOpacity(0.1)),
),
],
),
);
}

Widget _buildCandlestickChart(List<GraphPoint> points) {


final validPoints = points.where((p) => p.open != null && p.high != null && p.low
!= null && p.close != null).toList();

return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: max(MediaQuery.of(context).size.width, validPoints.length * 10.0),
child: BarChart(
BarChartData(
barGroups: validPoints.asMap().entries.map((entry) {
final p = entry.value;
final isUp = p.close! >= p.open!;
return BarChartGroupData(
x: entry.key,
barRods: [
BarChartRodData(
toY: p.high!,
fromY: p.low!,
rodStackItems: [
BarChartRodStackItem(p.low!, p.high!,
Colors.grey.withOpacity(0.3)),
BarChartRodStackItem(
min(p.open!, p.close!),
max(p.open!, p.close!),
isUp ? Colors.green : Colors.red,
),
],
width: 6,
),
],
);
}).toList(),
),
),
),
);
}

Widget _buildLoadingWidget() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text("Loading chart data..."),
],
),
);
}

Widget _buildErrorWidget(String message) {


return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.amber, size: 48),
SizedBox(height: 16),
Text(message, style: TextStyle(color: Colors.white)),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadData,
child: Text("Retry"),
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Provider.of<GraphProvider>(context).showCandlestick
? Icons.show_chart
: Icons.candlestick_chart),
onPressed: () {
Provider.of<GraphProvider>(context, listen: false).toggleChartType();
},
),
],
),
body: Consumer<GraphProvider>(
builder: (context, graphProvider, _) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: ToggleButtons(
isSelected: isSelected,
onPressed: _handleTimeframeChange,
children: const [
Text("1D"),
Text("7D"),
Text("28D"),
Text("90D"),
Text("365D"),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade800),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: _buildChart(context, graphProvider),
),
),
),
),
Consumer<MarketProvider>(
builder: (context, marketProvider, _) {
final coin = marketProvider.fetchCryptoById(widget.id);
return _buildCoinInfo(coin);
},
),
],
);
},
),
);
}

Widget _buildCoinInfo(CryptoCurrency coin) {


return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: CircleAvatar(backgroundImage: NetworkImage(coin.image!)),
title: Text("${coin.name} (${coin.symbol?.toUpperCase()})"),
subtitle: Text("₹ ${coin.currentPrice?.toStringAsFixed(4)}"),
),
Text(
"24h: ${coin.priceChange24h?.toStringAsFixed(2)}% "
"(${coin.priceChange24h?.toStringAsFixed(4)})",
style: TextStyle(
color: (coin.priceChange24h ?? 0) >= 0 ? Colors.green : Colors.red,
fontSize: 16,
),
),
],
),
);
}
}

Lib/pages/Favorites.dart
import 'package:crytoapp/models/Cryptocurrency.dart';
import 'package:crytoapp/providers/market_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../widgets/CryptoListTile.dart';

class Favorites extends StatefulWidget {


const Favorites({Key? key}) : super(key: key);

@override
State<Favorites> createState() => _FavoritesState();
}

class _FavoritesState extends State<Favorites> {


@override
Widget build(BuildContext context) {
return Consumer<MarketProvider>(
builder: (context, marketProvider, child) {
List<CryptoCurrency> favorites = marketProvider.markets
.where((element) => element.isFavorite == true)
.toList();
if (favorites.isNotEmpty) {
return ListView.builder(
itemCount: favorites.length,
itemBuilder: (context, index) {
CryptoCurrency currentCrypto = favorites[index];
return CryptoListTile(currentCrypto: currentCrypto);
},
);
}
else{
return const Center(child: Text("No Favorites yet",style:
TextStyle(color:Colors.grey,fontSize: 25),),);

}
},
);
// return Container(
// child: Text("Favorites HERE"),
// );
}
}

Lib/pages/HomePage.dart
import 'package:crytoapp/pages/DetailPage.dart';
import 'package:crytoapp/pages/Favorites.dart';
import 'package:crytoapp/pages/Market.dart';
import 'package:crytoapp/pages/news.dart';
import 'package:crytoapp/pages/Recommendations.dart'; // ⬅️ NEW
import 'package:crytoapp/providers/theme_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../widgets/Navbar.dart';

class HomePage extends StatefulWidget {


const HomePage({Key? key}) : super(key: key);

@override
State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with TickerProviderStateMixin {


late TabController viewController;

@override
void initState() {
super.initState();
viewController = TabController(length: 3, vsync: this);
}

@override
Widget build(BuildContext context) {
ThemeProvider themeProvider =
Provider.of<ThemeProvider>(context, listen: false);

return Scaffold(
drawer: Navbar(),
appBar: AppBar(
title: Padding(
padding: const EdgeInsets.all(40),
child: SizedBox(
height: 45,
child: Image.asset(
"assets/images/nametrans2.png",
fit: BoxFit.contain,
),
),
),
),
body: SafeArea(
child: Container(
padding: const EdgeInsets.only(top: 10, left: 10, right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
controller: viewController,
tabs: [
Tab(child: Text("Markets", style:
Theme.of(context).textTheme.bodyLarge)),
Tab(child: Text("Favorites", style:
Theme.of(context).textTheme.bodyLarge)),
Tab(child: Text("News", style:
Theme.of(context).textTheme.bodyLarge)),
],
),
Expanded(
child: TabBarView(
physics: const BouncingScrollPhysics(parent:
AlwaysScrollableScrollPhysics()),
controller: viewController,
children: const [Markets(), Favorites(), CryptoNewsList()],
),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const TradeRecommendationsPage()),
);
},
child: const Icon(Icons.trending_up),
backgroundColor: const Color(0xff0395eb),
tooltip: 'Top Trade Recommendations',
),
);
}
}

Lib/pages/Login.dart
import 'package:crytoapp/pages/HomePage.dart';
import 'package:crytoapp/pages/register.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart';

import '../providers/theme_provider.dart';

class LoginScreen extends StatefulWidget {


const LoginScreen({Key? key}) : super(key: key);

@override
_LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {


// form key
final _formKey = GlobalKey<FormState>();

// editing controller
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
// firebase
final _auth = FirebaseAuth.instance;

// string for displaying the error Message


String? errorMessage;

@override
Widget build(BuildContext context) {
ThemeProvider themeProvider =
Provider.of<ThemeProvider>(context, listen: false);
//email field
final emailField = TextFormField(
autofocus: false,
controller: emailController,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!.isEmpty) {
return ("Please Enter Your Email");
}
// reg expression for email validation
if (!RegExp("^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+.[a-z]")
.hasMatch(value)) {
return ("Please Enter a valid email");
}
return null;
},
onSaved: (value) {
emailController.text = value!;
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.mail),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "Email",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

//password field
final passwordField = TextFormField(
autofocus: false,
controller: passwordController,
obscureText: true,
validator: (value) {
RegExp regex = RegExp(r'^.{6,}$');
if (value!.isEmpty) {
return ("Password is required for login");
}
if (!regex.hasMatch(value)) {
return ("Enter Valid Password(Min. 6 Character)");
}
return null;
},
onSaved: (value) {
passwordController.text = value!;
},
textInputAction: TextInputAction.done,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.vpn_key),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

final loginButton = Material(


elevation: 5,
borderRadius: BorderRadius.circular(30),
color: const Color.fromARGB(255, 119, 99, 0),
child: MaterialButton(
padding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
minWidth: MediaQuery.of(context).size.width,
onPressed: () {
signIn(emailController.text, passwordController.text);
},
child: const Text(
"Login",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20, color: Colors.white, fontWeight: FontWeight.bold),
)),
);

return Scaffold(
body: Center(
child: SingleChildScrollView(
child: Container(
child: Padding(
padding: const EdgeInsets.all(36.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 100),
SizedBox(
height: 100,
child: Image.asset(
"assets/images/logo.png",
fit: BoxFit.contain,
)),
const SizedBox(height: 25),
SizedBox(
height: 50,
child: Image.asset(
"assets/images/name.png",
fit: BoxFit.contain,
)),
const SizedBox(height: 25),
emailField,
const SizedBox(height: 20),
passwordField,
const SizedBox(height: 25),
loginButton,
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Don't have an account? "),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const RegistrationScreen()));
},
child: const Row(
children: [
Text(
"SignUp",
style: TextStyle(
color: Colors.redAccent,
fontWeight: FontWeight.bold,
fontSize: 15),
),
],
),
)
]),
const SizedBox(height: 100),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Theme",
style: TextStyle(fontSize: 15),
),
IconButton(
onPressed: () {
themeProvider.toggleTheme();
},
icon: (themeProvider.themeMode == ThemeMode.light)
? const Icon(Icons.lightbulb_sharp)
: const Icon(Icons.lightbulb_sharp),
),
],
),
],
),
),
),
),
),
),
);
}

// login function
void signIn(String email, String password) async {
if (_formKey.currentState!.validate()) {
try {
await _auth
.signInWithEmailAndPassword(email: email, password: password)
.then((uid) => {
Fluttertoast.showToast(msg: "Login Successful"),
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomePage())),
});
} on FirebaseAuthException catch (error) {
switch (error.code) {
case "invalid-email":
errorMessage = "Your email address appears to be malformed.";

break;
case "wrong-password":
errorMessage = "Your password is wrong.";
break;
case "user-not-found":
errorMessage = "User with this email doesn't exist.";
break;
case "user-disabled":
errorMessage = "User with this email has been disabled.";
break;
case "too-many-requests":
errorMessage = "Too many requests";
break;
case "operation-not-allowed":
errorMessage = "Signing in with Email and Password is not enabled.";
break;
default:
errorMessage = "An undefined Error happened.";
}
Fluttertoast.showToast(msg: errorMessage!);
print(error.code);
}
}
}
}

Lib/pages/Market.dart
import 'package:crytoapp/widgets/CryptoListTile.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/Cryptocurrency.dart';
import '../providers/market_provider.dart';

class Markets extends StatefulWidget {


const Markets({Key? key}) : super(key: key);

@override
State<Markets> createState() => _MarketsState();
}

class _MarketsState extends State<Markets> {


@override
Widget build(BuildContext context) {
return Consumer<MarketProvider>(
builder: (context, marketProvider, child) {
if (marketProvider.isLoading && marketProvider.markets.isEmpty) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (marketProvider.markets.isNotEmpty) {
return RefreshIndicator(
onRefresh: () async {
await marketProvider.fetchData();
},
child: ListView.builder(
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemCount: marketProvider.markets.length,
itemBuilder: (context, index) {
CryptoCurrency currentCrypto = marketProvider.markets[index];
return CryptoListTile(currentCrypto: currentCrypto);
},
),
);
} else {
return const Center(
child: Text("Data Not Found!"),
);
}
},
);
}
}

Lib/pages/news.dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:readmore/readmore.dart';
import 'package:crytoapp/pages/news_webview.dart';

class CryptoNewsList extends StatefulWidget {


const CryptoNewsList({Key? key}) : super(key: key);

@override
_CryptoNewsListState createState() => _CryptoNewsListState();
}

class _CryptoNewsListState extends State<CryptoNewsList> {


List<dynamic> newsItems = [];
bool isLoading = true;
bool hasError = false;
final String apiKey = '07ebd2b791ee46f89787f29e849d9e3e';

@override
void initState() {
super.initState();
_fetchNews();
}

Future<void> _fetchNews() async {


try {
setState(() {
isLoading = true;
hasError = false;
});

final response = await http.get(


Uri.parse('https://newsapi.org/v2/everything?
q=cryptocurrency&apiKey=$apiKey&pageSize=20'),
).timeout(const Duration(seconds: 15));

if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
newsItems = data['articles'] ?? [];
isLoading = false;
});
} else {
throw Exception('Failed to load news: ${response.statusCode}');
}
} catch (e) {
setState(() {
isLoading = false;
hasError = true;
});
debugPrint('Error fetching news: $e');
}
}

void _openFullStory(BuildContext context, String url) {


Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewsWebView(url: url),
),
);
}

Widget _buildNewsItem(Map<String, dynamic> item, BuildContext context) {


final title = item['title'] ?? 'No title available';
final source = item['source']?['name'] ?? 'Unknown source';
final description = item['description'] ?? 'No description available';
final imageUrl = item['urlToImage'] ?? '';
final publishedAt = item['publishedAt'] ?? '';
final url = item['url'] ?? '';

return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (imageUrl.isNotEmpty)
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: Stack(
children: [
Image.network(
imageUrl,
height: 180,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
height: 180,
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.article, size: 50, color: Colors.grey),
),
),
),
Positioned(
bottom: 12,
left: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 119, 99, 0),
borderRadius: BorderRadius.circular(8),
),
child: Text(
source,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
),
if (publishedAt.isNotEmpty)
Text(
_formatDate(publishedAt),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).hintColor,
),
),
],
),
const SizedBox(height: 12),
ReadMoreText(
description,
trimLines: 3,
trimMode: TrimMode.Line,
colorClickableText: const Color.fromARGB(255, 119, 99, 0),
trimCollapsedText: 'Show more',
trimExpandedText: 'Show less',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _openFullStory(context, url),
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 119, 99, 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
'READ FULL STORY',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
),
);
}

String _formatDate(String isoDate) {


try {
final date = DateTime.parse(isoDate);
return '${date.day}/${date.month}/${date.year}';
} catch (e) {
return '';
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: RefreshIndicator(
onRefresh: _fetchNews,
backgroundColor: const Color.fromARGB(255, 119, 99, 0),
color: Colors.white,
displacement: 40,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverAppBar(
title: const Text('Crypto News'),
pinned: true,
elevation: 0,
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
),
if (isLoading)
const SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color.fromARGB(255, 119, 99, 0),
),
),
),
)
else if (hasError)
SliverFillRemaining(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 50,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 20),
Text(
'Failed to load news',
style: TextStyle(
fontSize: 18,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _fetchNews,
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 119, 99, 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: const Text(
'Retry',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
)
else if (newsItems.isEmpty)
const SliverFillRemaining(
child: Center(
child: Text(
'No news available',
style: TextStyle(fontSize: 16),
),
),
)
else
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildNewsItem(newsItems[index], context),
childCount: newsItems.length,
),
),
],
),
),
);
}
}

lib/news/news_webview.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

class NewsWebView extends StatefulWidget {


final String url;
const NewsWebView({Key? key, required this.url}) : super(key: key);

@override
State<NewsWebView> createState() => _NewsWebViewState();
}

class _NewsWebViewState extends State<NewsWebView> {


late final WebViewController _controller;
bool _isLoading = true;

@override
void initState() {
super.initState();
_initializeWebView();
}

void _initializeWebView() {
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}

final WebViewController controller =


WebViewController.fromPlatformCreationParams(params);

if (controller.platform is AndroidWebViewController) {
(controller.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}

controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
if (progress == 100) {
setState(() => _isLoading = false);
}
},
onPageStarted: (String url) => setState(() => _isLoading = true),
onPageFinished: (String url) => setState(() => _isLoading = false),
onWebResourceError: (WebResourceError error) {
setState(() => _isLoading = false);
debugPrint('WebView error: ${error.description}');
},
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.url));

_controller = controller;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('News Article'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
),
body: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color.fromARGB(255, 119, 99, 0),
),
),
)],
),
);
}
}

lib/pages/Recommendations.dart
// lib/pages/Recommendations.dart
// lib/pages/Recommendations.dart
import 'package:flutter/material.dart';
import 'package:crytoapp/models/cryptocurrency.dart';
import 'package:crytoapp/models/GraphPoint.dart';
import 'package:crytoapp/utils/recommender.dart';
import 'package:provider/provider.dart';
import 'package:crytoapp/providers/market_provider.dart';
import 'package:crytoapp/providers/graph_provider.dart';

class TradeRecommendationsPage extends StatelessWidget {


const TradeRecommendationsPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final marketProvider = Provider.of<MarketProvider>(context);
final graphProvider = Provider.of<GraphProvider>(context);

// Build historicalData Map from graphProvider cache


final Map<String, List<GraphPoint>> history = {
for (var crypto in marketProvider.markets)
crypto.id!: graphProvider.getGraphPoints(crypto.id!)
};
final recommendations = getTradeRecommendations(marketProvider.markets, history);

return Scaffold(
appBar: AppBar(
title: const Text("Trade Recommendations"),
backgroundColor: const Color.fromARGB(255, 119, 99, 0),
),
body: recommendations.isEmpty
? const Center(
child: Text(
"No recommendations available.",
style: TextStyle(fontSize: 18),
),
)
: ListView.builder(
itemCount: recommendations.length,
itemBuilder: (context, index) {
final rec = recommendations[index];
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.transparent,
backgroundImage: NetworkImage(rec.crypto1.image ?? ""),
radius: 12,
),
const SizedBox(width: 8),
Text(
rec.recommendationType == "PAIR_TRADE"
? "${rec.crypto1.symbol?.toUpperCase()}/$
{rec.crypto2?.symbol?.toUpperCase()}"
: rec.crypto1.symbol?.toUpperCase() ?? "",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const Spacer(),
Chip(
label: Text(
rec.recommendationType == "BUY"
? "BUY"
: rec.recommendationType == "SELL"
? "SELL"
: "PAIR",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
backgroundColor: rec.recommendationType == "BUY"
? Colors.green
: rec.recommendationType == "SELL"
? Colors.red
: Colors.blue,
),
const SizedBox(width: 8),
Text(
"Score: ${rec.score.toStringAsFixed(1)}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
const SizedBox(height: 8),
if (rec.recommendationType == "PAIR_TRADE") ...[
Row(
children: [
Text(
"Buy: ${rec.crypto1.symbol?.toUpperCase()}",
style: const TextStyle(fontWeight: FontWeight.w600),
),
const Spacer(),
Text(
"Sell: ${rec.crypto2?.symbol?.toUpperCase()}",
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 8),
],
Text(
rec.explanation,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
"₹${rec.crypto1.currentPrice?.toStringAsFixed(2)}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
if (rec.recommendationType == "PAIR_TRADE") ...[
const Spacer(),
Text(
"₹${rec.crypto2?.currentPrice?.toStringAsFixed(2)}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
],
),
],
),
),
);
},
),
);
}
}
Lib/pages/Portfolio.dart

Lib/pages/Register.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:crytoapp/models/user_model.dart';
import 'package:crytoapp/pages/HomePage.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

class RegistrationScreen extends StatefulWidget {


const RegistrationScreen({Key? key}) : super(key: key);

@override
_RegistrationScreenState createState() => _RegistrationScreenState();
}

class _RegistrationScreenState extends State<RegistrationScreen> {


final _auth = FirebaseAuth.instance;

// string for displaying the error Message


String? errorMessage;

// our form key


final _formKey = GlobalKey<FormState>();
// editing Controller
final firstNameEditingController = TextEditingController();
final secondNameEditingController = TextEditingController();
final emailEditingController = TextEditingController();
final passwordEditingController = TextEditingController();
final confirmPasswordEditingController = TextEditingController();

@override
Widget build(BuildContext context) {
//first name field
final firstNameField = TextFormField(
autofocus: false,
controller: firstNameEditingController,
keyboardType: TextInputType.name,
validator: (value) {
RegExp regex = RegExp(r'^.{3,}$');
if (value!.isEmpty) {
return ("First Name cannot be Empty");
}
if (!regex.hasMatch(value)) {
return ("Enter Valid name(Min. 3 Character)");
}
return null;
},
onSaved: (value) {
firstNameEditingController.text = value!;
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_circle),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "First Name",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

//second name field


final secondNameField = TextFormField(
autofocus: false,
controller: secondNameEditingController,
keyboardType: TextInputType.name,
validator: (value) {
if (value!.isEmpty) {
return ("Second Name cannot be Empty");
}
return null;
},
onSaved: (value) {
secondNameEditingController.text = value!;
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_circle),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "Second Name",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

//email field
final emailField = TextFormField(
autofocus: false,
controller: emailEditingController,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!.isEmpty) {
return ("Please Enter Your Email");
}
// reg expression for email validation
if (!RegExp("^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+.[a-z]")
.hasMatch(value)) {
return ("Please Enter a valid email");
}
return null;
},
onSaved: (value) {
firstNameEditingController.text = value!;
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.mail),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "Email",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

//password field
final passwordField = TextFormField(
autofocus: false,
controller: passwordEditingController,
obscureText: true,
validator: (value) {
RegExp regex = RegExp(r'^.{6,}$');
if (value!.isEmpty) {
return ("Password is required for login");
}
if (!regex.hasMatch(value)) {
return ("Enter Valid Password(Min. 6 Character)");
}
return null;
},
onSaved: (value) {
firstNameEditingController.text = value!;
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.vpn_key),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

//confirm password field


final confirmPasswordField = TextFormField(
autofocus: false,
controller: confirmPasswordEditingController,
obscureText: true,
validator: (value) {
if (confirmPasswordEditingController.text !=
passwordEditingController.text) {
return "Password don't match";
}
return null;
},
onSaved: (value) {
confirmPasswordEditingController.text = value!;
},
textInputAction: TextInputAction.done,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.vpn_key),
contentPadding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
hintText: "Confirm Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
));

//signup button
final signUpButton = Material(
elevation: 5,
borderRadius: BorderRadius.circular(30),
color: const Color.fromARGB(255, 119, 99, 0),
child: MaterialButton(
padding: const EdgeInsets.fromLTRB(20, 15, 20, 15),
minWidth: MediaQuery.of(context).size.width,
onPressed: () {
signUp(emailEditingController.text, passwordEditingController.text);
},
child: const Text(
"SignUp",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20, color: Colors.white, fontWeight: FontWeight.bold),
)),
);

return Scaffold(

appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Color.fromARGB(255, 119, 99, 0),),
onPressed: () {
// passing this to our root
Navigator.of(context).pop();
},
),
),
body: Center(
child: SingleChildScrollView(
child: Container(

child: Padding(
padding: const EdgeInsets.all(36.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 10),
SizedBox(
height: 100,
child: Image.asset(
"assets/images/logo.png",
fit: BoxFit.contain,
)),
const SizedBox(height: 25),
SizedBox(
height: 50,
child: Image.asset(
"assets/images/name.png",
fit: BoxFit.contain,
)),
const SizedBox(height: 25),
firstNameField,
const SizedBox(height: 20),
secondNameField,
const SizedBox(height: 20),
emailField,
const SizedBox(height: 20),
passwordField,
const SizedBox(height: 20),
confirmPasswordField,
const SizedBox(height: 20),
signUpButton,
const SizedBox(height: 15),
],
),
),
),
),
),
),
);
}
void signUp(String email, String password) async {
if (_formKey.currentState!.validate()) {
try {
await _auth
.createUserWithEmailAndPassword(email: email, password: password)
.then((value) => {postDetailsToFirestore()})
.catchError((e) {
Fluttertoast.showToast(msg: e!.message);
});
} on FirebaseAuthException catch (error) {
switch (error.code) {
case "invalid-email":
errorMessage = "Your email address appears to be malformed.";
break;
case "wrong-password":
errorMessage = "Your password is wrong.";
break;
case "user-not-found":
errorMessage = "User with this email doesn't exist.";
break;
case "user-disabled":
errorMessage = "User with this email has been disabled.";
break;
case "too-many-requests":
errorMessage = "Too many requests";
break;
case "operation-not-allowed":
errorMessage = "Signing in with Email and Password is not enabled.";
break;
default:
errorMessage = "An undefined Error happened.";
}
Fluttertoast.showToast(msg: errorMessage!);
print(error.code);
}
}
}
postDetailsToFirestore() async {
// calling our firestore
// calling our user model
// sedning these values

FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;


User? user = _auth.currentUser;

UserModel userModel = UserModel();

// writing all the values


userModel.email = user!.email;
userModel.uid = user.uid;
userModel.firstName = firstNameEditingController.text;
userModel.secondName = secondNameEditingController.text;

await firebaseFirestore
.collection("users")
.doc(user.uid)
.set(userModel.toMap());
Fluttertoast.showToast(msg: "Account created successfully :) ");
Navigator.pushAndRemoveUntil(
(context),
MaterialPageRoute(builder: (context) => const HomePage()),
(route) => false);
}
}

Lib/providers/graph_provider.dart
import 'package:crytoapp/models/GraphPoint.dart';
import 'package:crytoapp/models/API.dart';
import 'package:flutter/material.dart';

class GraphProvider with ChangeNotifier {


Map<String, List<GraphPoint>> graphCache = {};
Map<String, List<GraphPoint>> candlestickCache = {};

bool showCandlestick = false;


bool isLoading = false;
String? errorMessage;

Future<void> initializeGraph(String id, int days) async {


try {
isLoading = true;
errorMessage = null;
notifyListeners();

// Fetch both types of data in parallel


final results = await Future.wait([
_fetchLineChartData(id, days),
_fetchCandlestickData(id, days),
], eagerError: true);

graphCache[id] = results[0];
candlestickCache[id] = results[1];

debugPrint("Data loaded - Line: ${results[0].length}, Candlestick: $


{results[1].length}");
} catch (e) {
errorMessage = "Failed to load chart data: ${e.toString()}";
debugPrint("Error initializing graph data for $id: $e");
} finally {
isLoading = false;
notifyListeners();
}
}

Future<List<GraphPoint>> _fetchLineChartData(String id, int days) async {


try {
List<dynamic> priceData = await API.fetchGraphData(id, days);
return priceData.map((pricePoint) => GraphPoint.fromList(pricePoint)).toList();
} catch (e) {
debugPrint("Error fetching line data: $e");
return [];
}
}

Future<List<GraphPoint>> _fetchCandlestickData(String id, int days) async {


try {
List<dynamic> candlestickData = await API.fetchCandlestickData(id, days);
return candlestickData.map((candle) => GraphPoint.fromOHLC(candle)).toList();
} catch (e) {
debugPrint("Error fetching candlestick data: $e");
return [];
}
}

List<GraphPoint> getGraphPoints(String id) {


return graphCache[id] ?? [];
}

List<GraphPoint> getCandlestickPoints(String id) {


return candlestickCache[id] ?? [];
}

void toggleChartType() {
showCandlestick = !showCandlestick;
notifyListeners();
}
}

Lib/providers/market_provider.dart
import 'dart:async';

import 'package:crytoapp/models/API.dart';

import 'package:crytoapp/models/Cryptocurrency.dart';

import 'package:crytoapp/models/LocalStorage.dart';

import 'package:flutter/cupertino.dart';

class MarketProvider with ChangeNotifier {

bool isLoading = true;

List<CryptoCurrency> markets = [];

Timer? _timer;

MarketProvider() {

fetchData();

_startAutoRefresh(); // Start periodic updates

void _startAutoRefresh() {

_timer = Timer.periodic(const Duration(seconds: 15), (_) {

fetchData();
});

Future<void> fetchData() async {

try {

List<dynamic> _markets = await API.getMarkets();

if (_markets.isEmpty) return; // Prevent clearing data on API failure

List<String> favorites = await LocalStorage.fetchFavorites();

List<CryptoCurrency> temp = [];

for (var market in _markets) {

CryptoCurrency newCrypto = CryptoCurrency.fromJSON(market);

if (favorites.contains(newCrypto.id!)) {

newCrypto.isFavorite = true;

temp.add(newCrypto);

markets = temp;

isLoading = false;

notifyListeners();

} catch (e) {

debugPrint("Error fetching market data: $e");

// Don't overwrite existing data on error

@override
void dispose() {

_timer?.cancel();

super.dispose();

CryptoCurrency fetchCryptoById(String id) {

return markets.firstWhere((element) => element.id == id);

void addFavorite(CryptoCurrency crypto) async {

int indexOfCrypto = markets.indexOf(crypto);

markets[indexOfCrypto].isFavorite = true;

await LocalStorage.addFavorite(crypto.id!);

notifyListeners();

void removeFavorite(CryptoCurrency crypto) async {

int indexOfCrypto = markets.indexOf(crypto);

markets[indexOfCrypto].isFavorite = false;

await LocalStorage.removeFavorite(crypto.id!);

notifyListeners();

Lib/providers/theme_provider.dart
import 'package:crytoapp/models/LocalStorage.dart';

import 'package:flutter/material.dart';

class ThemeProvider with ChangeNotifier {


late ThemeMode themeMode;

ThemeProvider(String theme) {

if (theme == "light") {

themeMode = ThemeMode.light;

} else {

themeMode = ThemeMode.dark;

void toggleTheme() async {

if (themeMode == ThemeMode.light) {

themeMode = ThemeMode.dark;

await LocalStorage.saveTheme("dark");

} else {

themeMode = ThemeMode.light;

await LocalStorage.saveTheme("light");

notifyListeners();

}
Lib/utils/indicators.dart
// //lib/utils/indicators.dart
// lib/utils/indicators.dart
import 'package:crytoapp/models/GraphPoint.dart';

double calculateRSI(List<GraphPoint> points, {int period = 14}) {


if (points.length < period + 1) return 50.0;

List<double> gains = [];


List<double> losses = [];

for (int i = 1; i <= period; i++) {


double prev = points[i - 1].price ?? 0.0;
double current = points[i].price ?? 0.0;
double change = current - prev;

if (change >= 0) {
gains.add(change);
} else {
losses.add(change.abs());
}
}

double avgGain = gains.fold(0.0, (double a, double b) => a + b) / period;


double avgLoss = losses.fold(0.0, (double a, double b) => a + b) / period;

if (avgLoss == 0) return 100;

double rs = avgGain / avgLoss;


return 100 - (100 / (1 + rs));
}

double calculateSMA(List<GraphPoint> points, int period) {


if (points.length < period) return 0.0;
double sum = 0.0;

for (int i = 0; i < period; i++) {


sum += points[i].price ?? 0.0;
}

return sum / period;


}

double calculateEMA(List<GraphPoint> points, int period) {


if (points.isEmpty) return 0.0;

double multiplier = 2 / (period + 1);


double ema = points[0].price ?? 0.0;

for (int i = 1; i < points.length; i++) {


ema = ((points[i].price ?? 0.0) - ema) * multiplier + ema;
}

return ema;
}

double calculateMACD(List<GraphPoint> points) {


if (points.length < 26) return 0.0;

double ema12 = calculateEMA(points, 12);


double ema26 = calculateEMA(points, 26);
return ema12 - ema26;
}

Lib/utils/recommender.dart
import 'package:crytoapp/models/Cryptocurrency.dart';
import 'package:crytoapp/models/GraphPoint.dart';
import 'package:crytoapp/utils/indicators.dart';
import 'dart:math';

class TradeRecommendation {
final CryptoCurrency crypto1;
final CryptoCurrency? crypto2; // For pairs
final double score;
final String explanation;
final String recommendationType; // "BUY", "SELL", or "PAIR_TRADE"

TradeRecommendation({
required this.crypto1,
this.crypto2,
required this.score,
required this.explanation,
required this.recommendationType,
});
}

List<TradeRecommendation> getTradeRecommendations(
List<CryptoCurrency> marketData,
Map<String, List<GraphPoint>> historicalData,
) {
List<TradeRecommendation> recommendations = [];

// First pass - analyze individual coins


for (var crypto in marketData) {
final List<GraphPoint>? points = historicalData[crypto.id];
if (points == null || points.length < 15) continue;

double rsi = calculateRSI(points);


double sma7 = calculateSMA(points, 7);
double sma30 = calculateSMA(points, 30);
double price = crypto.currentPrice ?? 0.0;
double volume = crypto.totalVolume ?? 0.0;
double priceChange24h = crypto.priceChangePercentage24 ?? 0;

// Scoring logic
double score = 0;
String explanation = "";

// RSI analysis
if (rsi < 30) {
score += 2;
explanation += "Oversold (RSI ${rsi.toStringAsFixed(1)}). ";
} else if (rsi > 70) {
score -= 2;
explanation += "Overbought (RSI ${rsi.toStringAsFixed(1)}). ";
} else {
score += 1;
}

// Moving average crossover


if (sma7 > sma30 && price > sma7) {
score += 2;
explanation += "Bullish trend (price above 7 & 30-day MA). ";
} else if (sma7 < sma30 && price < sma7) {
score -= 2;
explanation += "Bearish trend (price below 7 & 30-day MA). ";
}

// Volume analysis
if (volume > 1000000000) { // 1B volume threshold
score += 1;
explanation += "High trading volume. ";
}

// Price momentum
if (priceChange24h > 5) {
score += 1;
explanation += "Strong upward momentum (+${priceChange24h.toStringAsFixed(1)}%).
";
} else if (priceChange24h < -5) {
score -= 1;
explanation += "Downward momentum (${priceChange24h.toStringAsFixed(1)}%). ";
}

if (score >= 3) {
recommendations.add(TradeRecommendation(
crypto1: crypto,
score: score,
explanation: explanation,
recommendationType: "BUY",
));
} else if (score <= -3) {
recommendations.add(TradeRecommendation(
crypto1: crypto,
score: score,
explanation: explanation,
recommendationType: "SELL",
));
}
}

// Second pass - find pairs with correlation opportunities


if (recommendations.length >= 2) {
// Sort by strongest recommendations
recommendations.sort((a, b) => b.score.abs().compareTo(a.score.abs()));

// Find best buy/sell pairs


List<TradeRecommendation> buyRecommendations = recommendations
.where((r) => r.recommendationType == "BUY")
.toList();

List<TradeRecommendation> sellRecommendations = recommendations


.where((r) => r.recommendationType == "SELL")
.toList();

// Create pair recommendations (limit to top 3 pairs)


int pairCount = min(3, min(buyRecommendations.length,
sellRecommendations.length));
for (int i = 0; i < pairCount; i++) {
final buy = buyRecommendations[i];
final sell = sellRecommendations[i];

double pairScore = (buy.score + sell.score.abs()) / 2;

recommendations.add(TradeRecommendation(
crypto1: buy.crypto1,
crypto2: sell.crypto1,
score: pairScore,
explanation: "Pair trade opportunity: Consider buying $
{buy.crypto1.symbol?.toUpperCase()} "
"(score: ${buy.score.toStringAsFixed(1)}) and selling $
{sell.crypto1.symbol?.toUpperCase()} "
"(score: ${sell.score.toStringAsFixed(1)}). ${buy.explanation} $
{sell.explanation}",
recommendationType: "PAIR_TRADE",
));
}
}

// Sort all recommendations by score


recommendations.sort((a, b) => b.score.compareTo(a.score));

return recommendations;
}

Lib/widgets/CryptoListTile.dart

import 'package:crytoapp/models/Cryptocurrency.dart';
import 'package:crytoapp/providers/market_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../pages/DetailPage.dart';

class CryptoListTile extends StatelessWidget {


final CryptoCurrency currentCrypto;

const CryptoListTile({Key? key, required this.currentCrypto})


: super(key: key);

@override
Widget build(BuildContext context) {

MarketProvider marketProvider =
Provider.of<MarketProvider>(context, listen: false);
return Column(
children: [
ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsPage(

id: currentCrypto.id!,
)),
);
},
tileColor: const Color.fromARGB(19, 92, 92, 92),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 00),
leading: CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: NetworkImage(currentCrypto.image!),
),
title: Row(
children: [
Flexible(
child:
Text(currentCrypto.name!, overflow: TextOverflow.ellipsis)),
const SizedBox(
width: 10,
),
(currentCrypto.isFavorite == false)
? GestureDetector(
onTap: () {
marketProvider.addFavorite(currentCrypto);
},
child: const Icon(
CupertinoIcons.heart,
size: 18,
),
)
: GestureDetector(
onTap: () {
marketProvider.removeFavorite(currentCrypto);
},
child: const Icon(
CupertinoIcons.heart_fill,
size: 20,
color: Colors.red,
),
),
],
),
subtitle: Text(currentCrypto.symbol!.toUpperCase()),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"₹ " + currentCrypto.currentPrice!.toStringAsFixed(4),
style: const TextStyle(
color: Color(0xff0395eb),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
Builder(
builder: (context) {
double priceChange = currentCrypto.priceChange24!;
double priceChangePercentage =
currentCrypto.priceChangePercentage24!;

if (priceChange < 0) {
// negative
return Text(
"${priceChangePercentage.toStringAsFixed(2)}% ($
{priceChange.toStringAsFixed(4)})",
style: const TextStyle(color: Colors.red),
);
} else {
// positive
return Text(
"+${priceChangePercentage.toStringAsFixed(2)}% (+$
{priceChange.toStringAsFixed(4)})",
style: const TextStyle(color: Colors.green),
);
}
},
),
],
),
),
const SizedBox(
height: 5,
),],
);
}
}

Lib/widgets/Navbar.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:crytoapp/providers/theme_provider.dart';
import 'package:provider/provider.dart';
import '../models/user_model.dart';
import '../pages/Login.dart';

class Navbar extends StatefulWidget {


const Navbar({Key? key}) : super(key: key);

@override
State<Navbar> createState() => _NavbarState();
}

class _NavbarState extends State<Navbar> {


User? user = FirebaseAuth.instance.currentUser;
UserModel loggedInUser = UserModel();

@override
void initState() {
super.initState();
FirebaseFirestore.instance
.collection("users")
.doc(user!.uid)
.get()
.then((value) {
loggedInUser = UserModel.fromMap(value.data());
setState(() {});
});
}

@override
Widget build(BuildContext context) {
ThemeProvider themeProvider =
Provider.of<ThemeProvider>(context, listen: false);
return Drawer(
child: ListView(
padding: const EdgeInsets.all(0),
children: [
const SizedBox(
height: 60.0,
),
Row(mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
SizedBox(
height: 60,

child: Image.asset(
"assets/images/logo.png",
fit: BoxFit.contain,
)),
IconButton(
onPressed: () {
themeProvider.toggleTheme();
},

icon: (themeProvider.themeMode == ThemeMode.light)


? const Icon(Icons.dark_mode_sharp)
: const Icon(Icons.light_mode_sharp),
),
],
),
ListTile(
leading: const Icon(Icons.account_circle_rounded),
title: Text("${loggedInUser.firstName} $
{loggedInUser.secondName}".toUpperCase(),),
onTap: () {
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.mail),
title: Text("${loggedInUser.email} ",),
onTap: () {
Navigator.of(context).pop();
},
),
InkWell(
onTap: () {
// Use PushNamed Function of Navigator class to push the named route
Navigator.pushNamed(context, 'login');
},
child: ListTile(
leading: const Icon(Icons.logout),
title: const Text('Log out'),
onTap: ()
{
logout(context);
}),

),
ListTile(

leading: const Icon(Icons.settings),


title: const Text('Settings'),
onTap: () {
Navigator.of(context).pop();
},
),
const SizedBox(height: 400,),
const ListTile(
title:

Row(
children:[
SizedBox(width: 40),
Text('MADE WITH '),
Icon(CupertinoIcons.heart_fill,color:Colors.red ,size: 18,),
Row(children:[Text(" BY PRADEEP"),]),],

),),

],
),
);

}
}
Future<void> logout(BuildContext context) async {
await FirebaseAuth.instance.signOut();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const LoginScreen()));
}

lib/widgets/candlestick_chart_widget.dart
// lib/widgets/candlestick_chart_widget.dart
import 'package:flutter/material.dart';
import 'package:candlesticks/candlesticks.dart';
import 'package:crytoapp/models/candle_data.dart';

class CandlestickChartWidget extends StatelessWidget {


final List<CandleData> candlePoints;

const CandlestickChartWidget({Key? key, required this.candlePoints}) : super(key:


key);

@override
Widget build(BuildContext context) {
if (candlePoints.isEmpty || candlePoints.any((e) =>
e.open.isNaN || e.high.isNaN || e.low.isNaN || e.close.isNaN)) {
return const Center(
child: Text(
"Failed to load chart data.",
style: TextStyle(color: Colors.red),
),
);
}

List<Candle> candles = candlePoints.map((e) => Candle(


date: e.time,
high: e.high,
low: e.low,
open: e.open,
close: e.close,
volume: 0,
)).toList();

return Container(
height: 400,
child: Candlesticks(
candles: candles,
interval: "1d",
onIntervalChange: (String _) async {
return;
},
),
);
}
}

Lib/services/recommendation_service.dart
import 'package:crytoapp/models/Cryptocurrency.dart';

class RecommendationService {
static String getBestPair(List<CryptoCurrency> markets) {
if (markets.length < 2) return "Not enough data";
markets.sort((a, b) =>
b.priceChangePercentage24!.compareTo(a.priceChangePercentage24!));
return "${markets[0].symbol!.toUpperCase()} / $
{markets[1].symbol!.toUpperCase()}";
}
}

Lib/main.dart
import 'package:animated_splash_screen/animated_splash_screen.dart';
import 'package:crytoapp/constants/Themes.dart';
import 'package:crytoapp/providers/graph_provider.dart';
import 'package:crytoapp/models/LocalStorage.dart';
import 'package:crytoapp/pages/Login.dart';
import 'package:crytoapp/providers/market_provider.dart';
import 'package:crytoapp/providers/theme_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
import 'package:page_transition/page_transition.dart';
import 'firebase_options.dart';

Future<void> main() async {


WidgetsFlutterBinding.ensureInitialized();

// Initialize WebView platform


if (WebViewPlatform.instance == null) {
if (defaultTargetPlatform == TargetPlatform.android) {
WebViewPlatform.instance = AndroidWebViewPlatform();
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
WebViewPlatform.instance = WebKitWebViewPlatform();
}
}

try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
String currentTheme = await LocalStorage.getTheme() ?? "light";
runApp(MyApp(theme: currentTheme));
} catch (e) {
runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: Text('Failed to initialize Firebase: $e'),
),
),
),
);
}
}

class MyApp extends StatelessWidget {


final String theme;
const MyApp({Key? key, required this.theme}) : super(key: key);

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MarketProvider>(
create: (context) => MarketProvider(),
),
ChangeNotifierProvider<GraphProvider>(
create: (context) => GraphProvider(),
),
ChangeNotifierProvider<ThemeProvider>(
create: (context) => ThemeProvider(theme),
),
],
child: Consumer<ThemeProvider>(
builder: (context, ThemeProvider, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeProvider.themeMode,
theme: lightTheme,
darkTheme: darkTheme,
home: const SplashScreen(),
);
},
),
);
}
}

class SplashScreen extends StatelessWidget {


const SplashScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return AnimatedSplashScreen(
splash: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset("assets/images/logo.png"),
],
),
backgroundColor: const Color.fromARGB(255, 119, 99, 0),
nextScreen: const LoginScreen(),
splashIconSize: 200,
duration: 1000,
splashTransition: SplashTransition.rotationTransition,
pageTransitionType: PageTransitionType.bottomToTop,
animationDuration: const Duration(seconds: 3),
);
}
}

crypto_tracker_flutter/pubspec.yaml
name: crytoapp
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as
versionCode.
# Read more about Android versioning at
https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as
CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/
InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1

environment:
sdk: ">=2.16.1 <3.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
firebase_auth: ^4.17.4
cloud_firestore: ^4.15.5
firebase_core: ^2.30.0
cupertino_icons: ^1.0.2
page_transition: "^2.0.5"
connectivity_plus: 3.0.3
flutter_dotenv: ^5.1.0
webview_flutter: ^3.0.4
webview_flutter_android: ^2.8.2
webview_flutter_wkwebview: ^2.7.1
web_socket_channel: ^2.4.0
# syncfusion_flutter_charts: ^20.2.43 # Known stable version
# syncfusion_flutter_core: ^20.2.43
fl_chart: ^0.66.0
intl: ^0.18.1
flutter:
sdk: flutter
fluttertoast: ^8.0.9
http: ^0.13.4
provider: ^6.0.2
readmore: ^2.1.0
shared_preferences: ^2.0.13
animated_splash_screen: ^1.1.0
shimmer: ^2.0.0
# syncfusion_flutter_charts: ^20.4.48

dependency_overrides:
# syncfusion_flutter_core: ^20.2.43

dev_dependencies:
flutter_lints: ^1.0.0
flutter_test:
sdk: flutter
flutter_launcher_icons: "^0.9.2"

flutter_icons:
android: true
ios: true
image_path: "assets/images/logo.png"

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:

# The following line ensures that the Material Icons font is


# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/logo.png
- assets/images/name.png
- assets/images/nametrans.png
- assets/images/nametrans2.png
- assets/app_logo.png
- .env
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

The interface the provider tell me about the app is

You might also like