Flutter Currency Tracker Tutorial (plus React Native): Build an Iranian Gold and FX App
A Flutter currency tracker tutorial for live Iranian dollar, gold and coin rates: backend proxy, models, polling, offline cache, RTL and Persian digits.


- The app talks to your own backend (which holds the EZRate key), never to the API directly.
- A small repository class polls once a minute while the app is in the foreground and caches the last response for offline use.
- RTL layout, Persian digits and clear units make the app feel native to Iranian users.
A rate tracker is one of the most downloaded app categories in Iran. This Flutter currency tracker tutorial builds a clean, production-minded app for live Tehran dollar, gold and coin prices, and shows the React Native equivalent of the key parts.
/rates from your backend (proxying https://api.ezrate.me/v1/rates with your key and a 60-second cache). In Flutter, fetch it with http, parse into a Rate model, poll every minute only while the app is resumed, store the last response with shared_preferences for offline start, and format with intl for Persian digits and RTL.Part of the Iranian Rial exchange rate API guide. Persian readers: Flutter currency app.
1. Backend first
Do not embed the key in the app; binaries can be decompiled. Use the proxy from securing financial API keys and point the app at https://your-backend.example/rates.
2. Model
class Rate {
final String symbol, name;
final int buy, sell;
final double changePct;
final bool lowSources;
final DateTime updatedAt;
Rate.fromJson(Map<String, dynamic> j)
: symbol = j["symbol"],
name = j["name"],
buy = (j["buy"] as num).round(),
sell = (j["sell"] as num).round(),
changePct = (j["change_pct"] as num).toDouble(),
lowSources = j["low_sources"] == true,
updatedAt = DateTime.parse(j["updated_at"]);
}3. Repository with offline cache
import "dart:convert";
import "package:http/http.dart" as http;
import "package:shared_preferences/shared_preferences.dart";
class RatesRepo {
static const _url = "https://your-backend.example/rates";
static const _key = "rates_cache";
Future<List<Rate>> cached() async {
final s = (await SharedPreferences.getInstance()).getString(_key);
return s == null ? [] : _parse(s);
}
Future<List<Rate>> fetch() async {
final r = await http.get(Uri.parse(_url)).timeout(const Duration(seconds: 10));
if (r.statusCode != 200) throw Exception("HTTP ${r.statusCode}");
(await SharedPreferences.getInstance()).setString(_key, r.body);
return _parse(r.body);
}
List<Rate> _parse(String body) =>
(jsonDecode(body)["data"] as List).map((e) => Rate.fromJson(e)).toList();
}4. Lifecycle-aware polling
class RatesController extends ChangeNotifier with WidgetsBindingObserver {
final repo = RatesRepo();
List<Rate> rates = [];
bool stale = false;
Timer? _t;
Future<void> start() async {
WidgetsBinding.instance.addObserver(this);
rates = await repo.cached();
notifyListeners();
_tick();
_t = Timer.periodic(const Duration(minutes: 1), (_) => _tick());
}
Future<void> _tick() async {
try { rates = await repo.fetch(); stale = false; } catch (_) { stale = true; }
notifyListeners();
}
@override
void didChangeAppLifecycleState(AppLifecycleState s) {
if (s == AppLifecycleState.resumed) { _tick(); _t ??= Timer.periodic(const Duration(minutes: 1), (_) => _tick()); }
if (s == AppLifecycleState.paused) { _t?.cancel(); _t = null; }
}
}Stopping the timer in the background saves battery and requests. For home-screen widgets and push alerts you need platform services; EZRate’s own mobile app is an example of the result.
5. UI: RTL, digits and units
final fa = NumberFormat.decimalPattern("fa");
ListTile(
title: Text(rate.name),
subtitle: Text(rate.lowSources ? "کممنبع" : ""),
trailing: Directionality(
textDirection: TextDirection.ltr,
child: Text("${fa.format(rate.sell)} تومان",
style: TextStyle(color: rate.changePct >= 0 ? Colors.green : Colors.red)),
),
);- Set
locale: Locale("fa")andsupportedLocales; Flutter flips layouts automatically. - Wrap numbers in LTR
Directionalityso signs and separators stay in place. - Always print the unit; see IRR vs Toman.
React Native equivalent
import { useEffect, useState } from "react";
import { AppState, FlatList, Text } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export function useRates() {
const [rates, setRates] = useState<any[]>([]);
useEffect(() => {
let timer: any;
const load = async () => {
try {
const r = await fetch("https://your-backend.example/rates");
const body = await r.text();
await AsyncStorage.setItem("rates", body);
setRates(JSON.parse(body).data);
} catch {}
};
AsyncStorage.getItem("rates").then(s => s && setRates(JSON.parse(s).data));
const start = () => { load(); timer = setInterval(load, 60_000); };
start();
const sub = AppState.addEventListener("change", s => { clearInterval(timer); if (s === "active") start(); });
return () => { clearInterval(timer); sub.remove(); };
}, []);
return rates;
}Architecture of the app
The app is deliberately thin. Your backend talks to EZRate, caches rates for a minute and exposes a small JSON endpoint. The repository class in the app fetches that endpoint and stores the last response on the device. The controller owns the refresh timer and pauses it in the background. The widgets only render what the controller exposes. This separation makes each layer testable: you can unit-test parsing with a fixture, test the controller with a fake repository, and run widget tests without any network.
Going further
- Favourites: let users pin assets; store the list locally and request only those symbols.
- Charts: add
/history/{symbol}through your backend and draw withfl_chart(Flutter) orvictory-native(React Native). - Price alerts: run the alert logic on your server and deliver via FCM/APNs; polling on the device is unreliable in the background.
- Home-screen widgets: need native code (Android App Widgets, iOS WidgetKit) that reads the cached snapshot.
- Store review: state the data source and update time in the app and in the listing to avoid “misleading data” rejections.
Persian readers have a step-by-step Flutter-only guide in Flutter currency app.
Performance and battery
Rate apps are opened many times a day for a few seconds, so start-up time matters more than anything else. Render the cached snapshot immediately and refresh in the background; the user sees numbers at once and they update a moment later. Keep the payload small by requesting only the symbols on screen, and avoid timers in the background, which both drain the battery and are killed by the operating system anyway. On Android, test with battery saver on; on iOS, test after the app has been suspended for a while.
Finally, design for poor connections. Show the time of the cached data clearly, retry with backoff when the network returns, and never replace good cached values with an empty list after a failed request.
Related live rates: US dollar rate, 18K gold price, Emami coin price.

Comments (0)
Comments are shown after review. Sign-in is needed to send.


