Skip to main content

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});
}