Dart Examples
Complete Dart examples for integrating OpenFXRates using the http package and async patterns, suitable for Flutter and Dart applications.
Installation
Add the following dependencies to your pubspec.yaml:
dependencies:
http: ^1.1.0
flutter_dotenv: ^5.1.0 # For environment variables
dev_dependencies:
test: ^1.24.0
Then run:
dart pub get
# or for Flutter projects
flutter pub get
Example 1: Get Latest Rates
import 'dart:convert';
import 'package:http/http.dart' as http;
const String apiKey = 'your-api-key-here';
const String baseUrl = 'https://api.openfxrates.com';
class LatestRatesResponse {
final String base;
final String date;
final Map<String, double> rates;
LatestRatesResponse({
required this.base,
required this.date,
required this.rates,
});
factory LatestRatesResponse.fromJson(Map<String, dynamic> json) {
return LatestRatesResponse(
base: json['base'] as String,
date: json['date'] as String,
rates: (json['rates'] as Map<String, dynamic>)
.map((key, value) => MapEntry(key, (value as num).toDouble())),
);
}
}
Future<LatestRatesResponse> getLatestRates(String base, String targets) async {
final uri = Uri.parse('$baseUrl/latest_rates').replace(queryParameters: {
'base': base,
'targets': targets,
});
final response = await http.get(
uri,
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
final data = LatestRatesResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
print('Base: ${data.base}');
print('Date: ${data.date}');
print('Rates:');
data.rates.forEach((currency, rate) {
print(' $currency: ${rate.toStringAsFixed(4)}');
});
return data;
} else {
throw Exception('API Error ${response.statusCode}: ${response.body}');
}
}
void main() async {
try {
await getLatestRates('USD', 'EUR,GBP,JPY');
} catch (e) {
print('Error: $e');
}
}
Example 2: Convert Currency
import 'dart:convert';
import 'package:http/http.dart' as http;
class ConversionResponse {
final String from;
final double amount;
final Map<String, double> conversions;
ConversionResponse({
required this.from,
required this.amount,
required this.conversions,
});
factory ConversionResponse.fromJson(Map<String, dynamic> json) {
return ConversionResponse(
from: json['from'] as String,
amount: (json['amount'] as num).toDouble(),
conversions: (json['conversions'] as Map<String, dynamic>)
.map((key, value) => MapEntry(key, (value as num).toDouble())),
);
}
}
Future<ConversionResponse> convertCurrency(
String from,
String to,
double amount,
) async {
final uri = Uri.parse('$baseUrl/convert').replace(queryParameters: {
'from': from,
'to': to,
'amount': amount.toString(),
});
final response = await http.get(
uri,
headers: {'X-API-Key': apiKey},
);
if (response.statusCode == 200) {
final data = ConversionResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
print('Converting: ${data.amount.toStringAsFixed(2)} ${data.from}');
print('Results:');
data.conversions.forEach((currency, converted) {
print(' $currency: ${converted.toStringAsFixed(2)}');
});
return data;
} else {
throw Exception('Conversion error: ${response.body}');
}
}
void main() async {
try {
await convertCurrency('USD', 'EUR,GBP,JPY', 100.0);
} catch (e) {
print('Error: $e');
}
}
Example 3: Get Historical Rates
import 'dart:convert';
import 'package:http/http.dart' as http;
class HistoricalRatesResponse {
final String base;
final String date;
final Map<String, double> rates;
HistoricalRatesResponse({
required this.base,
required this.date,
required this.rates,
});
factory HistoricalRatesResponse.fromJson(Map<String, dynamic> json) {
return HistoricalRatesResponse(
base: json['base'] as String,
date: json['date'] as String,
rates: (json['rates'] as Map<String, dynamic>)
.map((key, value) => MapEntry(key, (value as num).toDouble())),
);
}
}
Future<HistoricalRatesResponse> getHistoricalRates(
String base,
String date,
String targets,
) async {
final uri = Uri.parse('$baseUrl/historical_rates').replace(queryParameters: {
'base': base,
'date': date,
'targets': targets,
});
final response = await http.get(
uri,
headers: {'X-API-Key': apiKey},
);
if (response.statusCode == 200) {
final data = HistoricalRatesResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
print('Historical rates for ${data.date}');
print('Base: ${data.base}');
print('Rates:');
data.rates.forEach((currency, rate) {
print(' $currency: ${rate.toStringAsFixed(4)}');
});
return data;
} else {
throw Exception('API Error: ${response.body}');
}
}
void main() async {
try {
// Get rates from January 15, 2024
await getHistoricalRates('USD', '2024-01-15', 'EUR,GBP');
} catch (e) {
print('Error: $e');
}
}
Example 4: List All Currencies
import 'dart:convert';
import 'package:http/http.dart' as http;
class Currency {
final String code;
final String name;
final String? symbol;
Currency({
required this.code,
required this.name,
this.symbol,
});
factory Currency.fromJson(Map<String, dynamic> json) {
return Currency(
code: json['code'] as String,
name: json['name'] as String,
symbol: json['symbol'] as String?,
);
}
}
class CurrenciesResponse {
final List<Currency> currencies;
CurrenciesResponse({required this.currencies});
factory CurrenciesResponse.fromJson(Map<String, dynamic> json) {
return CurrenciesResponse(
currencies: (json['currencies'] as List<dynamic>)
.map((item) => Currency.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
}
Future<List<Currency>> listCurrencies() async {
final uri = Uri.parse('$baseUrl/currencies');
final response = await http.get(
uri,
headers: {'X-API-Key': apiKey},
);
if (response.statusCode == 200) {
final data = CurrenciesResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>,
);
print('Available currencies: ${data.currencies.length}\n');
for (final currency in data.currencies) {
final symbol = currency.symbol != null ? ' (${currency.symbol})' : '';
print('${currency.code} - ${currency.name}$symbol');
}
return data.currencies;
} else {
throw Exception('Error listing currencies: ${response.body}');
}
}
void main() async {
try {
await listCurrencies();
} catch (e) {
print('Error: $e');
}
}
Example 5: API Client Class
Complete reusable client class:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
class OpenFXRatesClient {
final String apiKey;
final String baseUrl;
final http.Client httpClient;
OpenFXRatesClient({
required this.apiKey,
String? baseUrl,
http.Client? httpClient,
}) : baseUrl = baseUrl ?? 'https://api.openfxrates.com',
httpClient = httpClient ?? http.Client();
factory OpenFXRatesClient.fromEnv() {
final apiKey = dotenv.env['OPENFXRATES_API_KEY'];
if (apiKey == null || apiKey.isEmpty) {
throw Exception('OPENFXRATES_API_KEY not found in environment');
}
return OpenFXRatesClient(apiKey: apiKey);
}
Future<LatestRatesResponse> getLatestRates(
String base, {
String? targets,
}) async {
final params = <String, String>{'base': base};
if (targets != null) {
params['targets'] = targets;
}
final response = await _makeRequest('/latest_rates', params);
return LatestRatesResponse.fromJson(response);
}
Future<HistoricalRatesResponse> getHistoricalRates(
String base,
String date, {
String? targets,
}) async {
final params = <String, String>{
'base': base,
'date': date,
};
if (targets != null) {
params['targets'] = targets;
}
final response = await _makeRequest('/historical_rates', params);
return HistoricalRatesResponse.fromJson(response);
}
Future<ConversionResponse> convertCurrency(
String from,
String to,
double amount,
) async {
final params = <String, String>{
'from': from,
'to': to,
'amount': amount.toString(),
};
final response = await _makeRequest('/convert', params);
return ConversionResponse.fromJson(response);
}
Future<List<Currency>> listCurrencies() async {
final response = await _makeRequest('/currencies', {});
final data = CurrenciesResponse.fromJson(response);
return data.currencies;
}
Future<Map<String, dynamic>> _makeRequest(
String endpoint,
Map<String, String> queryParams,
) async {
final uri = Uri.parse('$baseUrl$endpoint')
.replace(queryParameters: queryParams);
final response = await httpClient.get(
uri,
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
return json.decode(response.body) as Map<String, dynamic>;
} else {
final errorBody = json.decode(response.body) as Map<String, dynamic>;
throw ApiException(
statusCode: response.statusCode,
message: errorBody['message'] as String? ?? 'Unknown error',
);
}
}
void dispose() {
httpClient.close();
}
}
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException({required this.statusCode, required this.message});
String toString() => 'ApiException($statusCode): $message';
}
Using the Client Class
void main() async {
final client = OpenFXRatesClient.fromEnv();
try {
// Get latest rates
final latest = await client.getLatestRates('USD', targets: 'EUR,GBP,JPY');
print('Base: ${latest.base}');
print('Rates: ${latest.rates}');
// Convert currency
final conversion = await client.convertCurrency('USD', 'EUR', 100.0);
print('Conversions: ${conversion.conversions}');
// Get historical rates
final historical = await client.getHistoricalRates(
'USD',
'2024-01-15',
targets: 'EUR,GBP',
);
print('Historical date: ${historical.date}');
print('Rates: ${historical.rates}');
// List currencies
final currencies = await client.listCurrencies();
print('Total currencies: ${currencies.length}');
} catch (e) {
print('Error: $e');
} finally {
client.dispose();
}
}
Example 6: Flutter Widget Integration
Using the API client in a Flutter application:
import 'package:flutter/material.dart';
class CurrencyConverterWidget extends StatefulWidget {
const CurrencyConverterWidget({Key? key}) : super(key: key);
State<CurrencyConverterWidget> createState() =>
_CurrencyConverterWidgetState();
}
class _CurrencyConverterWidgetState extends State<CurrencyConverterWidget> {
final _client = OpenFXRatesClient.fromEnv();
final _amountController = TextEditingController(text: '100');
String _fromCurrency = 'USD';
String _toCurrency = 'EUR';
double? _convertedAmount;
bool _isLoading = false;
String? _error;
Future<void> _convertCurrency() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final amount = double.parse(_amountController.text);
final result = await _client.convertCurrency(
_fromCurrency,
_toCurrency,
amount,
);
setState(() {
_convertedAmount = result.conversions[_toCurrency];
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Currency Converter')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Amount',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _fromCurrency,
decoration: const InputDecoration(
labelText: 'From',
border: OutlineInputBorder(),
),
items: ['USD', 'EUR', 'GBP', 'JPY']
.map((currency) => DropdownMenuItem(
value: currency,
child: Text(currency),
))
.toList(),
onChanged: (value) {
setState(() => _fromCurrency = value!);
},
),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: _toCurrency,
decoration: const InputDecoration(
labelText: 'To',
border: OutlineInputBorder(),
),
items: ['USD', 'EUR', 'GBP', 'JPY']
.map((currency) => DropdownMenuItem(
value: currency,
child: Text(currency),
))
.toList(),
onChanged: (value) {
setState(() => _toCurrency = value!);
},
),
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _convertCurrency,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Convert'),
),
const SizedBox(height: 24),
if (_convertedAmount != null)
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Result: ${_convertedAmount!.toStringAsFixed(2)} $_toCurrency',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
),
),
if (_error != null)
Card(
color: Colors.red[100],
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Error: $_error',
style: TextStyle(color: Colors.red[900]),
),
),
),
],
),
),
);
}
void dispose() {
_amountController.dispose();
_client.dispose();
super.dispose();
}
}
Example 7: Concurrent Requests
Making multiple API calls concurrently:
Future<void> fetchMultipleCurrencies() async {
final client = OpenFXRatesClient.fromEnv();
try {
final results = await Future.wait([
client.getLatestRates('USD', targets: 'EUR,GBP,JPY'),
client.getLatestRates('EUR', targets: 'USD,GBP,JPY'),
client.getLatestRates('GBP', targets: 'USD,EUR,JPY'),
]);
for (final result in results) {
print('\n${result.base} Rates:');
result.rates.forEach((currency, rate) {
print(' $currency: ${rate.toStringAsFixed(4)}');
});
}
} catch (e) {
print('Error: $e');
} finally {
client.dispose();
}
}
void main() async {
await fetchMultipleCurrencies();
}
Example 8: Error Handling with Custom Types
enum ApiErrorType {
invalidApiKey,
rateLimitExceeded,
notFound,
serverError,
networkError,
unknown,
}
class OpenFXRatesException implements Exception {
final ApiErrorType type;
final String message;
final int? statusCode;
OpenFXRatesException({
required this.type,
required this.message,
this.statusCode,
});
factory OpenFXRatesException.fromStatusCode(
int statusCode,
String message,
) {
final type = switch (statusCode) {
401 => ApiErrorType.invalidApiKey,
429 => ApiErrorType.rateLimitExceeded,
404 => ApiErrorType.notFound,
>= 500 => ApiErrorType.serverError,
_ => ApiErrorType.unknown,
};
return OpenFXRatesException(
type: type,
message: message,
statusCode: statusCode,
);
}
String toString() => 'OpenFXRatesException($type): $message';
}
Future<Map<String, dynamic>> makeRequestWithErrorHandling(
String url,
String apiKey,
) async {
try {
final response = await http.get(
Uri.parse(url),
headers: {'X-API-Key': apiKey},
);
if (response.statusCode == 200) {
return json.decode(response.body) as Map<String, dynamic>;
} else {
final errorBody = json.decode(response.body) as Map<String, dynamic>;
throw OpenFXRatesException.fromStatusCode(
response.statusCode,
errorBody['message'] as String? ?? 'Unknown error',
);
}
} on http.ClientException catch (e) {
throw OpenFXRatesException(
type: ApiErrorType.networkError,
message: 'Network error: ${e.message}',
);
} catch (e) {
if (e is OpenFXRatesException) rethrow;
throw OpenFXRatesException(
type: ApiErrorType.unknown,
message: e.toString(),
);
}
}
void main() async {
try {
final data = await makeRequestWithErrorHandling(
'https://api.openfxrates.com/latest_rates?base=USD&targets=EUR',
'your-api-key',
);
print('Success: $data');
} on OpenFXRatesException catch (e) {
switch (e.type) {
case ApiErrorType.invalidApiKey:
print('Error: Invalid API key. Please check your credentials.');
case ApiErrorType.rateLimitExceeded:
print('Error: Rate limit exceeded. Please try again later.');
case ApiErrorType.networkError:
print('Error: Network connection failed.');
default:
print('Error: ${e.message}');
}
}
}
Example 9: Retry Logic with Exponential Backoff
Future<T> retryWithBackoff<T>({
required Future<T> Function() operation,
int maxRetries = 3,
Duration initialDelay = const Duration(seconds: 1),
}) async {
int attempts = 0;
while (true) {
try {
return await operation();
} catch (e) {
attempts++;
if (attempts >= maxRetries) {
throw Exception('Max retries exceeded: $e');
}
final delay = initialDelay * (1 << attempts); // Exponential backoff
print('Request failed, retrying in $delay... (attempt $attempts/$maxRetries)');
await Future.delayed(delay);
}
}
}
void main() async {
final client = OpenFXRatesClient.fromEnv();
try {
final data = await retryWithBackoff(
operation: () => client.getLatestRates('USD', targets: 'EUR,GBP,JPY'),
maxRetries: 3,
);
print('Success: ${data.base}');
} catch (e) {
print('Error: $e');
} finally {
client.dispose();
}
}
Best Practices
1. Environment Variables
Create a .env file in your project root:
OPENFXRATES_API_KEY=your-api-key-here
Load it in your main.dart:
import 'package:flutter_dotenv/flutter_dotenv.dart';
Future<void> main() async {
await dotenv.load();
runApp(const MyApp());
}
2. Dependency Injection
Use dependency injection for better testing:
class CurrencyService {
final OpenFXRatesClient _client;
CurrencyService(this._client);
Future<LatestRatesResponse> getRates(String base) {
return _client.getLatestRates(base, targets: 'EUR,GBP,JPY');
}
}
// In tests
void main() {
test('getRates returns data', () async {
final mockClient = MockOpenFXRatesClient();
final service = CurrencyService(mockClient);
// Test implementation
});
}
3. State Management with Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CurrencyProvider extends ChangeNotifier {
final OpenFXRatesClient _client;
LatestRatesResponse? _latestRates;
bool _isLoading = false;
String? _error;
CurrencyProvider(this._client);
LatestRatesResponse? get latestRates => _latestRates;
bool get isLoading => _isLoading;
String? get error => _error;
Future<void> fetchRates(String base, String targets) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_latestRates = await _client.getLatestRates(base, targets: targets);
} catch (e) {
_error = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// Usage in widget
class RatesWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<CurrencyProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const CircularProgressIndicator();
}
if (provider.error != null) {
return Text('Error: ${provider.error}');
}
if (provider.latestRates != null) {
return Text('Rates: ${provider.latestRates!.rates}');
}
return const Text('No data');
},
);
}
}
4. Response Caching
class CachedClient {
final OpenFXRatesClient _client;
final Map<String, CacheEntry> _cache = {};
final Duration _cacheDuration;
CachedClient(this._client, {Duration? cacheDuration})
: _cacheDuration = cacheDuration ?? const Duration(minutes: 5);
Future<LatestRatesResponse> getLatestRatesCached(
String base, {
String? targets,
}) async {
final cacheKey = '$base:$targets';
if (_cache.containsKey(cacheKey)) {
final entry = _cache[cacheKey]!;
if (DateTime.now().difference(entry.timestamp) < _cacheDuration) {
return entry.data as LatestRatesResponse;
}
}
final data = await _client.getLatestRates(base, targets: targets);
_cache[cacheKey] = CacheEntry(data: data, timestamp: DateTime.now());
return data;
}
}
class CacheEntry {
final dynamic data;
final DateTime timestamp;
CacheEntry({required this.data, required this.timestamp});
}