currency_converter_pro 0.1.0
currency_converter_pro: ^0.1.0 copied to clipboard
Professional Currency and Crypto Converter for Flutter. Supports multi-providers, offline cache, historical rates, batch conversion, and real-time streams.
import 'package:flutter/material.dart';
import 'package:currency_converter_pro/currency_converter_pro.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Currency Converter Pro',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
primary: Colors.deepPurple,
secondary: Colors.amber,
),
useMaterial3: true,
cardTheme: CardThemeData(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
clipBehavior: Clip.antiAlias,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
primary: Colors.deepPurpleAccent,
),
useMaterial3: true,
),
home: const MainDemoScreen(),
);
}
}
class MainDemoScreen extends StatefulWidget {
const MainDemoScreen({super.key});
@override
State<MainDemoScreen> createState() => _MainDemoScreenState();
}
class _MainDemoScreenState extends State<MainDemoScreen> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Theme.of(context).colorScheme.primary.withOpacity(0.1),
Theme.of(context).colorScheme.surface,
],
),
),
child: Column(
children: [
const SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(Icons.currency_exchange, color: Colors.white, size: 32),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Currency Pro',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const Text('Ultimate Conversion Suite', style: TextStyle(color: Colors.grey)),
],
),
],
),
),
const SizedBox(height: 20),
TabBar(
labelColor: Theme.of(context).colorScheme.primary,
unselectedLabelColor: Colors.grey,
indicatorSize: TabBarIndicatorSize.label,
tabs: const [
Tab(icon: Icon(Icons.swap_vert), text: 'Convert'),
Tab(icon: Icon(Icons.insights), text: 'Live'),
Tab(icon: Icon(Icons.auto_awesome), text: 'Pro'),
],
),
const Expanded(
child: TabBarView(
children: [
CurrencyConverterTab(),
LiveRatesTab(),
AdvancedFeaturesTab(),
],
),
),
],
),
),
),
);
}
}
class CurrencyConverterTab extends StatefulWidget {
const CurrencyConverterTab({super.key});
@override
State<CurrencyConverterTab> createState() => _CurrencyConverterTabState();
}
class _CurrencyConverterTabState extends State<CurrencyConverterTab> {
final TextEditingController _amountController = TextEditingController(text: '100');
String _from = 'USD';
String _to = 'INR';
ConversionResult? _result;
bool _isLoading = false;
final _converter = CurrencyConverterPro(cacheDuration: const Duration(hours: 1));
Future<void> _convert() async {
setState(() => _isLoading = true);
await Future.delayed(const Duration(milliseconds: 500)); // Smooth feel
try {
final res = await _converter.convertCurrency(
amount: double.tryParse(_amountController.text) ?? 0,
fromCurrency: _from,
toCurrency: _to,
);
if (mounted) setState(() => _result = res);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
decoration: InputDecoration(
labelText: 'Amount to convert',
hintText: '0.00',
prefixIcon: const Icon(Icons.account_balance_wallet_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
),
),
const SizedBox(height: 24),
Row(
children: [
_buildCurrencyButton(context, _from, true),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: IconButton(
icon: const Icon(Icons.swap_horiz),
onPressed: () {
setState(() {
final temp = _from;
_from = _to;
_to = temp;
});
},
),
),
),
_buildCurrencyButton(context, _to, false),
],
),
],
),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 60,
child: FilledButton.icon(
onPressed: _isLoading ? null : _convert,
icon: _isLoading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: const Icon(Icons.bolt),
label: const Text('Convert Now', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
),
),
if (_result != null) ...[
const SizedBox(height: 32),
_buildResultCard(context),
]
],
),
);
}
Widget _buildCurrencyButton(BuildContext context, String code, bool isFrom) {
final currency = CurrencyData.allCurrencies.firstWhere((c) => c.code == code,
orElse: () => Currency(code: code, name: '', symbol: '', flag: '🏳️'));
return Expanded(
child: InkWell(
onTap: () => CurrencyPickerWidget.show(
context: context,
favorite: const ['USD', 'EUR', 'INR', 'GBP'],
onSelect: (c) => setState(() {
if (isFrom) _from = c.code; else _to = c.code;
}),
),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).colorScheme.outline.withOpacity(0.2)),
),
child: Column(
children: [
Text(currency.flag, style: const TextStyle(fontSize: 32)),
const SizedBox(height: 4),
Text(code, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
],
),
),
),
);
}
Widget _buildResultCard(BuildContext context) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 400),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: Card(
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Text(
_result!.formattedAmount,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
_ResultDetail(Icons.analytics, 'Rate', '1 $_from = ${_result!.rate} $_to'),
_ResultDetail(Icons.source, 'Provider', _result!.provider),
_ResultDetail(Icons.history, 'Source', _result!.isCached ? 'Local Cache' : 'Real-time API'),
],
),
),
),
);
}
}
class LiveRatesTab extends StatefulWidget {
const LiveRatesTab({super.key});
@override
State<LiveRatesTab> createState() => _LiveRatesTabState();
}
class _LiveRatesTabState extends State<LiveRatesTab> {
final _converter = CurrencyConverterPro();
String _base = 'USD';
final List<String> _targets = ['INR', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'];
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: InkWell(
onTap: () => CurrencyPickerWidget.show(
context: context,
onSelect: (c) => setState(() => _base = c.code),
),
child: Card(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Icon(Icons.currency_bitcoin),
const SizedBox(width: 16),
Text('Base: $_base', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const Spacer(),
const Text('Change', style: TextStyle(color: Colors.blue)),
],
),
),
),
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _targets.length,
itemBuilder: (context, index) {
final target = _targets[index];
return StreamBuilder<double>(
stream: _converter.rateStream(
fromCurrency: _base,
toCurrency: target,
interval: const Duration(seconds: 10),
),
builder: (context, snapshot) {
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.green.withOpacity(0.1),
child: const Icon(Icons.trending_up, color: Colors.green),
),
title: Text('$_base to $target', style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: const Text('Auto-updates every 10s'),
trailing: snapshot.hasData
? Text(
snapshot.data!.toStringAsFixed(4),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 18,
),
)
: const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
),
);
},
);
},
),
),
],
);
}
}
class AdvancedFeaturesTab extends StatefulWidget {
const AdvancedFeaturesTab({super.key});
@override
State<AdvancedFeaturesTab> createState() => _AdvancedFeaturesTabState();
}
class _AdvancedFeaturesTabState extends State<AdvancedFeaturesTab> {
final _converter = CurrencyConverterPro(cacheDuration: const Duration(minutes: 30));
Map<String, ConversionResult>? _batchResults;
DateTime _selectedDate = DateTime.now().subtract(const Duration(days: 30));
ConversionResult? _historicalResult;
bool _isBatchLoading = false;
bool _isHistoricalLoading = false;
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(24),
children: [
_buildFeatureCard(
context,
'Batch Conversion',
'Convert one currency to many in a single efficient call.',
Icons.layers,
Colors.orange,
Column(
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _isBatchLoading ? null : () async {
setState(() => _isBatchLoading = true);
try {
final res = await _converter.convertMany(
amount: 100,
fromCurrency: 'USD',
toCurrencies: const ['EUR', 'GBP', 'INR', 'JPY'],
);
if (mounted) setState(() => _batchResults = res);
} finally {
if (mounted) setState(() => _isBatchLoading = false);
}
},
child: _isBatchLoading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Run Batch (100 USD)'),
),
),
if (_batchResults != null) ...[
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: _batchResults!.entries.map((e) => Chip(
avatar: CircleAvatar(child: Text(e.key[0])),
label: Text('${e.key}: ${e.value.formattedAmount}'),
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
)).toList(),
),
]
],
),
),
const SizedBox(height: 20),
_buildFeatureCard(
context,
'Historical Analysis',
'Access market rates from any date in history.',
Icons.history_toggle_off,
Colors.blue,
Column(
children: [
ListTile(
contentPadding: EdgeInsets.zero,
title: Text('${_selectedDate.year}-${_selectedDate.month}-${_selectedDate.day}'),
trailing: const Icon(Icons.calendar_month),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (date != null && mounted) setState(() => _selectedDate = date);
},
),
SizedBox(
width: double.infinity,
child: FilledButton.tonal(
onPressed: _isHistoricalLoading ? null : () async {
setState(() => _isHistoricalLoading = true);
try {
final res = await _converter.convertCurrency(amount: 1, fromCurrency: 'USD', toCurrency: 'EUR', date: _selectedDate);
if (mounted) setState(() => _historicalResult = res);
} finally {
if (mounted) setState(() => _isHistoricalLoading = false);
}
},
child: _isHistoricalLoading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Fetch Historical Rate'),
),
),
if (_historicalResult != null) ...[
const SizedBox(height: 12),
Text('Rate was: ${_historicalResult!.rate} EUR', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
]
],
),
),
const SizedBox(height: 20),
_buildFeatureCard(
context,
'Performance & Fallback',
'Manage local cache and test provider redundancy.',
Icons.speed,
Colors.green,
Row(
children: [
Expanded(
child: ActionButton(
icon: Icons.refresh,
label: 'Force Refresh',
onTap: () {
_converter.forceRefresh();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Next request will bypass cache')));
},
),
),
const SizedBox(width: 8),
Expanded(
child: ActionButton(
icon: Icons.delete_outline,
label: 'Clear Cache',
onTap: () async {
await _converter.clearCache();
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Local cache cleared')));
},
),
),
],
),
),
],
);
}
Widget _buildFeatureCard(BuildContext context, String title, String sub, IconData icon, Color color, Widget content) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color),
const SizedBox(width: 12),
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
Text(sub, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
const SizedBox(height: 16),
content,
],
),
),
);
}
}
class _ResultDetail extends StatelessWidget {
final IconData icon;
final String label;
final String value;
const _ResultDetail(this.icon, this.label, this.value);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary.withOpacity(0.7)),
const SizedBox(width: 12),
Text(label, style: const TextStyle(color: Colors.grey)),
const Spacer(),
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
);
}
}
class ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
const ActionButton({super.key, required this.icon, required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline.withOpacity(0.2)),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon, size: 24),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 11)),
],
),
),
);
}
}