اپ قیمت ارز با فلاتر (Flutter)؛ از JSON تا رابط کاربری راستچین
ساخت اپ قیمت ارز با فلاتر: مدل داده، دریافت JSON، سرور واسط برای حفظ کلید، RefreshIndicator، کش آفلاین، اعداد فارسی و رابط راستچین برای اندروید و iOS.

- اپ موبایل نباید کلید API را داخل خود داشته باشد؛ اپ از سرور شما بخواند و سرور از API.
- با پکیج http و یک کلاس مدل، JSON نرخها را به لیست اشیای Dart تبدیل کنید و با FutureBuilder یا Riverpod نمایش دهید.
- آخرین پاسخ را در shared_preferences ذخیره کنید تا اپ بدون اینترنت هم آخرین نرخ را با زمانش نشان دهد.
فلاتر به دلیل یک کد برای اندروید و iOS، انتخاب محبوبی برای اپهای قیمت ارز و طلاست. در این آموزش یک اپ قیمت ارز با فلاتر میسازیم که نرخ دلار، یورو، طلا، سکه و تتر را نشان میدهد، با کشیدن صفحه به پایین تازه میشود، بدون اینترنت آخرین نرخ را نگه میدارد و رابطش کاملاً راستچین و فارسی است.
https://yourapp.com/rates) که با کلید از api.ezrate.me/v1/rates بخواند و کش کند. اپ فلاتر با پکیج http همان مسیر را میخواند، JSON را به List<Rate> تبدیل میکند و در ListView نمایش میدهد. آخرین پاسخ در shared_preferences ذخیره میشود.این آموزش بخشی از راهنمای API قیمت است.
معماری: کلید هرگز داخل اپ نمیرود
هر فایل APK یا IPA را میتوان باز کرد و رشتههای داخل آن را خواند. اگر کلید API را در کد Dart بگذارید، دیر یا زود استخراج و سوءاستفاده میشود. راه درست یک لایه سرور کوچک است که هم کلید را نگه میدارد و هم پاسخ را برای همه کاربران کش میکند. این سرور میتواند همان بکاند اپ شما باشد یا یک تابع سرورلس ساده. جزئیات و نمونهٔ پروکسی در امنیت API Key در فرانتاند آمده است.
گام ۱: وابستگیها
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
http: ^1.2.0
intl: ^0.19.0
shared_preferences: ^2.2.0گام ۲: مدل داده
class Rate {
final String symbol, name;
final num buy, sell, changePct;
final int sources;
final DateTime updatedAt;
Rate.fromJson(Map<String, dynamic> j)
: symbol = j['symbol'],
name = j['name'],
buy = j['buy'],
sell = j['sell'],
changePct = j['change_pct'] ?? 0,
sources = j['sources'] ?? 0,
updatedAt = DateTime.parse(j['updated_at']);
}گام ۳: سرویس دریافت نرخ با کش آفلاین
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class RatesService {
static const _url = 'https://yourapp.com/rates'; // سرور خودتان، نه api.ezrate.me
static const _cacheKey = 'last_rates';
Future<List<Rate>> load() async {
final prefs = await SharedPreferences.getInstance();
try {
final res = await http.get(Uri.parse(_url)).timeout(const Duration(seconds: 8));
if (res.statusCode == 200) {
await prefs.setString(_cacheKey, res.body); // آخرین پاسخ معتبر
return _parse(res.body);
}
} catch (_) {/* بدون اینترنت یا خطای سرور */}
final cached = prefs.getString(_cacheKey);
return cached == null ? [] : _parse(cached);
}
List<Rate> _parse(String body) =>
(jsonDecode(body)['data'] as List).map((e) => Rate.fromJson(e)).toList();
}گام ۴: صفحه نرخها با رابط راستچین
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
final _fa = NumberFormat.decimalPattern('fa');
class RatesPage extends StatefulWidget {
const RatesPage({super.key});
@override
State<RatesPage> createState() => _RatesPageState();
}
class _RatesPageState extends State<RatesPage> {
final _service = RatesService();
List<Rate> _rates = [];
Future<void> _refresh() async {
final r = await _service.load();
if (mounted) setState(() => _rates = r);
}
@override
void initState() { super.initState(); _refresh(); }
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('قیمت لحظهای بازار')),
body: RefreshIndicator(
onRefresh: _refresh,
child: ListView.separated(
itemCount: _rates.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) {
final r = _rates[i];
final up = r.changePct >= 0;
return ListTile(
title: Text(r.name),
subtitle: Text('${r.sources} منبع'),
trailing: Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
Text('${_fa.format(r.sell)} تومان', style: const TextStyle(fontWeight: FontWeight.bold)),
Text('${up ? '▲' : '▼'} ${_fa.format(r.changePct.abs())}٪',
style: TextStyle(color: up ? Colors.green : Colors.red)),
]),
);
},
),
),
);
}برای راستچین شدن کامل، در MaterialApp مقدار locale: const Locale('fa') و supportedLocales و localizationsDelegates را تنظیم کنید. فلاتر جهت متن و چیدمان را خودش برعکس میکند. فونت فارسی مناسب (مثل وزیرمتن) را هم در pubspec.yaml اضافه کنید.
بهروزرسانی دورهای و مصرف باتری
وقتی صفحه باز است، میتوانید با یک Timer.periodic هر ۶۰ ثانیه _refresh را صدا بزنید و در dispose آن را لغو کنید. وقتی اپ در پسزمینه است، تازه کردن مداوم فقط باتری مصرف میکند؛ برای خبر کردن کاربر از تغییرات مهم، سرور شما باید شرط را بررسی و نوتیفیکیشن (FCM) بفرستد. دلیل این تصمیم در WebSocket یا REST برای قیمت لحظهای آمده است.
ویجت صفحه اصلی گوشی
کاربران اپهای قیمت، ویجت صفحهٔ اصلی (Home Screen Widget) را خیلی دوست دارند. فلاتر برای این کار پکیج home_widget دارد، اما بخش نمایش ویجت باید با کد بومی (Kotlin برای اندروید و SwiftUI برای iOS) نوشته شود. سادهترین الگو: اپ در هر تازهسازی نرخها را در حافظهٔ مشترک بنویسد و ویجت فقط از آن بخواند.
قبل از انتشار در فروشگاه
- در توضیحات اپ منبع نرخها را ذکر کنید و روشن بگویید نرخها جهت اطلاع است.
- اگر اپ پرداخت درونبرنامهای دارد، قوانین Google Play و App Store دربارهٔ خرید محتوای دیجیتال را بررسی کنید.
- اپ را با اینترنت قطع باز کنید: باید آخرین نرخ را با زمانش نشان دهد، نه صفحهٔ خالی.
- نسخه انگلیسی این آموزش، همراه با React Native، در Flutter & React Native FX Tracker آمده است.
انتشار اپ در فروشگاهها
قبل از انتشار در گوگلپلی، بازار یا مایکت چند نکته را بررسی کنید: نام و توضیح اپ نباید ادعای «نرخ رسمی» داشته باشد، منبع داده و زمان بهروزرسانی در اپ نمایش داده شود، و صفحهٔ حریم خصوصی داشته باشید. اگر اپ اعلان قیمت دارد، اجازهٔ اعلان را فقط وقتی کاربر هشدار میسازد درخواست کنید. برای اندروید، نسخهٔ APK آزمایشی را روی چند گوشی با اندازههای مختلف تست کنید تا جدولها در صفحههای کوچک بههم نریزند.

نظرات (۰)
نظرها پس از تأیید نمایش داده میشوند؛ برای ارسال، ورود به حساب لازم است.


