Currency converters are one of the most practical apps you can build while learning Flutter. They combine HTTP networking, state management, and interactive UI elements into a compact, real-world project. In this tutorial, you will build a complete currency converter app in Flutter using an exchange rate API that supports 160+ currencies.
By the end, you will have a working app with dropdown currency selectors, a text field for the amount, a convert button, and a clean result display -- all powered by live exchange rate data.
Why Choose This Exchange Rate API for Flutter?
When building a mobile app, you need an API that is fast, reliable, and easy to integrate. The Exchange Rate API checks all three boxes. It returns clean JSON, supports bearer token authentication, and offers a free tier with 1,500 requests per month -- more than enough for development and light production use. Using an exchange rate API in Flutter is straightforward because Dart's http package handles JSON responses natively.
Prerequisites
- Flutter SDK 3.x installed
- A code editor (VS Code or Android Studio)
- An API key from exchange-rateapi.com
- Basic familiarity with Dart and Flutter widgets
Project Setup
Create a new Flutter project and add the http package:
flutter create currency_converter
cd currency_converter
flutter pub add http
Add your API key to a constants file. In production, store this securely using flutter_dotenv or compile-time environment variables.
// lib/constants.dart
class AppConstants {
static const String apiKey = 'YOUR_API_KEY';
static const String baseUrl = 'https://api.allratestoday.com/v1';
}
Building the Data Model
Create a model to represent the API response. This makes your code type-safe and easier to maintain.
// lib/models/exchange_rate.dart
class ExchangeRateResponse {
final String base;
final Map<String, double> rates;
final String date;
ExchangeRateResponse({
required this.base,
required this.rates,
required this.date,
});
factory ExchangeRateResponse.fromJson(Map<String, dynamic> json) {
final rawRates = json['rates'] as Map<String, dynamic>;
final rates = rawRates.map(
(key, value) => MapEntry(key, (value as num).toDouble()),
);
return ExchangeRateResponse(
base: json['base'] as String,
rates: rates,
date: json['date'] as String,
);
}
}
class ConversionResult {
final String from;
final String to;
final double amount;
final double result;
final double rate;
ConversionResult({
required this.from,
required this.to,
required this.amount,
required this.result,
required this.rate,
});
factory ConversionResult.fromJson(Map<String, dynamic> json) {
return ConversionResult(
from: json['from'] as String,
to: json['to'] as String,
amount: (json['amount'] as num).toDouble(),
result: (json['result'] as num).toDouble(),
rate: (json['rate'] as num).toDouble(),
);
}
}
Creating the API Service
The service class encapsulates all HTTP calls to the exchange rate API. Flutter apps benefit from separating network logic from UI code.
// lib/services/exchange_rate_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../constants.dart';
import '../models/exchange_rate.dart';
class ExchangeRateService {
final http.Client _client;
ExchangeRateService({http.Client? client})
: _client = client ?? http.Client();
Map<String, String> get _headers => {
'Authorization': 'Bearer ${AppConstants.apiKey}',
'Accept': 'application/json',
};
/// Fetch latest rates for a base currency.
Future<ExchangeRateResponse> getLatestRates(String base) async {
final uri = Uri.parse('${AppConstants.baseUrl}/latest')
.replace(queryParameters: {'base': base});
final response = await _client.get(uri, headers: _headers);
if (response.statusCode != 200) {
throw ExchangeRateException(
'Failed to fetch rates: ${response.statusCode}',
);
}
return ExchangeRateResponse.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
/// Convert an amount from one currency to another.
Future<ConversionResult> convert({
required String from,
required String to,
required double amount,
}) async {
final uri = Uri.parse('${AppConstants.baseUrl}/convert').replace(
queryParameters: {
'from': from,
'to': to,
'amount': amount.toString(),
},
);
final response = await _client.get(uri, headers: _headers);
if (response.statusCode != 200) {
throw ExchangeRateException(
'Conversion failed: ${response.statusCode}',
);
}
return ConversionResult.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
void dispose() => _client.close();
}
class ExchangeRateException implements Exception {
final String message;
ExchangeRateException(this.message);
@override
String toString() => 'ExchangeRateException: $message';
}
Building the Currency Converter Screen
Now for the main event: the UI. This screen loads available currencies on startup, then lets the user pick a source and target currency, enter an amount, and tap "Convert."
// lib/screens/converter_screen.dart
import 'package:flutter/material.dart';
import '../models/exchange_rate.dart';
import '../services/exchange_rate_service.dart';
class ConverterScreen extends StatefulWidget {
const ConverterScreen({super.key});
@override
State<ConverterScreen> createState() => _ConverterScreenState();
}
class _ConverterScreenState extends State<ConverterScreen> {
final _service = ExchangeRateService();
final _amountController = TextEditingController(text: '100');
List<String> _currencies = [];
String _fromCurrency = 'USD';
String _toCurrency = 'EUR';
ConversionResult? _result;
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_loadCurrencies();
}
Future<void> _loadCurrencies() async {
setState(() => _isLoading = true);
try {
final response = await _service.getLatestRates('USD');
final codes = response.rates.keys.toList()..sort();
codes.insert(0, 'USD'); // Ensure base is included
setState(() {
_currencies = codes;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = 'Failed to load currencies. Check your connection.';
_isLoading = false;
});
}
}
Future<void> _convert() async {
final amount = double.tryParse(_amountController.text);
if (amount == null || amount <= 0) {
setState(() => _error = 'Please enter a valid amount.');
return;
}
setState(() {
_isLoading = true;
_error = null;
});
try {
final result = await _service.convert(
from: _fromCurrency,
to: _toCurrency,
amount: amount,
);
setState(() {
_result = result;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = 'Conversion failed. Please try again.';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Currency Converter'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(20),
child: _currencies.isEmpty && _isLoading
? const Center(child: CircularProgressIndicator())
: _buildForm(),
),
);
}
Widget _buildForm() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Amount input
TextField(
controller: _amountController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(
labelText: 'Amount',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
),
const SizedBox(height: 16),
// Currency selectors
Row(
children: [
Expanded(child: _buildDropdown('From', _fromCurrency, (val) {
setState(() => _fromCurrency = val!);
})),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: IconButton(
icon: const Icon(Icons.swap_horiz, size: 32),
onPressed: () {
setState(() {
final temp = _fromCurrency;
_fromCurrency = _toCurrency;
_toCurrency = temp;
});
},
),
),
Expanded(child: _buildDropdown('To', _toCurrency, (val) {
setState(() => _toCurrency = val!);
})),
],
),
const SizedBox(height: 24),
// Convert button
ElevatedButton(
onPressed: _isLoading ? null : _convert,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Convert', style: TextStyle(fontSize: 18)),
),
const SizedBox(height: 24),
// Result display
if (_error != null)
Text(_error!, style: const TextStyle(color: Colors.red)),
if (_result != null) _buildResultCard(),
],
);
}
Widget _buildDropdown(
String label,
String value,
ValueChanged<String?> onChanged,
) {
return DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
items: _currencies
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
.toList(),
onChanged: onChanged,
);
}
Widget _buildResultCard() {
final r = _result!;
return Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
'${r.amount.toStringAsFixed(2)} ${r.from}',
style: const TextStyle(fontSize: 18, color: Colors.grey),
),
const SizedBox(height: 8),
const Icon(Icons.arrow_downward),
const SizedBox(height: 8),
Text(
'${r.result.toStringAsFixed(2)} ${r.to}',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'1 ${r.from} = ${r.rate.toStringAsFixed(4)} ${r.to}',
style: const TextStyle(color: Colors.grey),
),
],
),
),
);
}
@override
void dispose() {
_amountController.dispose();
_service.dispose();
super.dispose();
}
}
Wiring Up main.dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'screens/converter_screen.dart';
void main() => runApp(const CurrencyConverterApp());
class CurrencyConverterApp extends StatelessWidget {
const CurrencyConverterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Currency Converter',
theme: ThemeData(
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const ConverterScreen(),
);
}
}
Adding Historical Rate Lookup
A nice feature to add is the ability to check what a conversion would have been on a past date. The exchange rate API for Flutter makes this simple with the /v1/historical endpoint.
// Add to ExchangeRateService
Future<ExchangeRateResponse> getHistoricalRates(
String date,
String base,
) async {
final uri = Uri.parse('${AppConstants.baseUrl}/historical').replace(
queryParameters: {'date': date, 'base': base},
);
final response = await _client.get(uri, headers: _headers);
if (response.statusCode != 200) {
throw ExchangeRateException(
'Failed to fetch historical rates: ${response.statusCode}',
);
}
return ExchangeRateResponse.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
You can add a date picker button to your UI and call this method to show how rates have changed over time.
Performance Tips
When using an exchange rate API in Flutter, keep these tips in mind:
- Cache responses locally. Use
shared_preferencesorhiveto store the last fetched rates. Show cached data immediately while refreshing in the background. - Minimize API calls. Fetch rates once on app launch and reuse them. The
/v1/latestendpoint returns all 160+ currencies in one call. - Handle offline gracefully. Check connectivity before making requests and fall back to cached data when offline.
- Debounce rapid taps. Disable the convert button while a request is in flight to prevent duplicate calls.
Testing the Service Layer
Dart makes it easy to inject a mock HTTP client for testing:
import 'package:http/testing.dart';
void main() {
test('convert returns correct result', () async {
final mockClient = MockClient((request) async {
return http.Response(
jsonEncode({
'from': 'USD',
'to': 'EUR',
'amount': 100,
'result': 92.15,
'rate': 0.9215,
}),
200,
);
});
final service = ExchangeRateService(client: mockClient);
final result = await service.convert(
from: 'USD',
to: 'EUR',
amount: 100,
);
expect(result.result, 92.15);
expect(result.rate, 0.9215);
});
}
Conclusion
You now have a fully functional currency converter app built in Flutter using a live exchange rate API. The architecture separates concerns cleanly: models handle data, the service handles networking, and the screen handles UI. This pattern scales well as you add features like favorites, rate alerts, or multi-currency comparisons.
The exchange rate API for Flutter used in this tutorial supports 160+ currencies and provides a free tier with 1,500 monthly requests, which is more than enough to develop, test, and ship a personal or small-scale production app.
Ready to build your own currency converter? Sign up for a free API key at exchange-rateapi.com and have your Flutter app fetching live rates in minutes. The API documentation covers every endpoint you need.
Start Using the Exchange Rate API Today
Free tier with 1,500 requests/month. 160+ currencies updated every 60 seconds. No credit card required.
Get Your Free API Key →