Crypto Tracker Flutter App - !
Crypto Tracker Flutter App - !
Lib/constants/Themes.dart
import 'package:flutter/material.dart';
);
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',
};
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
);
}
}
// Exponential backoff
await Future.delayed(initialRetryDelay * pow(1.5, attempt - 1) as Duration);
attempt++;
}
}
return [];
}
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;
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,
});
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,
});
return GraphPoint(
date: DateTime.fromMillisecondsSinceEpoch(list[0] as int),
price: (list[1] as num).toDouble(),
);
}
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;
}
Lib/models/user_model.dart
class UserModel {
String? uid;
String? email;
String? firstName;
String? 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';
@override
_DetailsPageState createState() => _DetailsPageState();
}
@override
void initState() {
super.initState();
_loadData();
}
void _loadData() {
final graphProvider = Provider.of<GraphProvider>(context, listen: false);
graphProvider.initializeGraph(widget.id, days);
}
if (graphProvider.getErrorMessage(widget.id) != null) {
return _buildErrorWidget(graphProvider.getErrorMessage(widget.id)!);
}
if (points.isEmpty) {
return _buildErrorWidget("No data available for selected period");
}
return graphProvider.showCandlestick
? _buildCandlestickChart(points)
: _buildLineChart(points);
}
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)),
),
],
),
);
}
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..."),
],
),
);
}
@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);
},
),
],
);
},
),
);
}
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';
@override
State<Favorites> createState() => _FavoritesState();
}
}
},
);
// 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';
@override
State<HomePage> createState() => _HomePageState();
}
@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';
@override
_LoginScreenState createState() => _LoginScreenState();
}
// editing controller
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
// firebase
final _auth = FirebaseAuth.instance;
@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),
),
));
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';
@override
State<Markets> createState() => _MarketsState();
}
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';
@override
_CryptoNewsListState createState() => _CryptoNewsListState();
}
@override
void initState() {
super.initState();
_fetchNews();
}
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');
}
}
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,
),
),
),
),
],
),
),
],
),
);
}
@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';
@override
State<NewsWebView> createState() => _NewsWebViewState();
}
@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();
}
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';
@override
Widget build(BuildContext context) {
final marketProvider = Provider.of<MarketProvider>(context);
final graphProvider = Provider.of<GraphProvider>(context);
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';
@override
_RegistrationScreenState createState() => _RegistrationScreenState();
}
@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),
),
));
//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),
),
));
//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
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';
graphCache[id] = results[0];
candlestickCache[id] = results[1];
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';
Timer? _timer;
MarketProvider() {
fetchData();
void _startAutoRefresh() {
fetchData();
});
try {
if (favorites.contains(newCrypto.id!)) {
newCrypto.isFavorite = true;
temp.add(newCrypto);
markets = temp;
isLoading = false;
notifyListeners();
} catch (e) {
@override
void dispose() {
_timer?.cancel();
super.dispose();
markets[indexOfCrypto].isFavorite = true;
await LocalStorage.addFavorite(crypto.id!);
notifyListeners();
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';
ThemeProvider(String theme) {
if (theme == "light") {
themeMode = ThemeMode.light;
} else {
themeMode = ThemeMode.dark;
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';
if (change >= 0) {
gains.add(change);
} else {
losses.add(change.abs());
}
}
return ema;
}
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 = [];
// 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;
}
// 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",
));
}
}
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",
));
}
}
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';
@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';
@override
State<Navbar> createState() => _NavbarState();
}
@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();
},
),
ListTile(
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';
@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),
),
);
}
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';
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'),
),
),
),
);
}
}
@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(),
);
},
),
);
}
}
@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: