شبکه و API
اپلیکیشنهای موبایل بدون backend ناکامل هستند. در این فصل یاد میگیریم چطور Flutter را به Django REST Framework متصل کنیم — REST API، JSON، JWT authentication و file upload.
۷.۱ مقدمه
اپلیکیشنهای موبایل بدون backend ناکامل هستند. در این فصل یاد میگیریم چطور Flutter را به Django REST Framework متصل کنیم — REST API، JSON، JWT authentication و file upload.
۷.۲ Package http — ساده و سبک
flutter pub add http
import "package:http/http.dart" as http;
import "dart:convert";
// GET ساده
Future> fetchProducts() async {
final url = Uri.parse("https://api.example.com/products/");
final response = await http.get(url);
if (response.statusCode == 200) {
return jsonDecode(response.body) as List;
} else {
throw Exception("Failed to load: ${response.statusCode}");
}
}
// POST با body
Future
۷.۳ Dio — قدرتمندتر و حرفهای
Dio package استاندارد industry برای HTTP در Flutter است. interceptors، caching، retry، file upload و لاگگیری دارد.
flutter pub add dio
import "package:dio/dio.dart";
class ApiClient {
late final Dio _dio;
ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: "https://api.shop.com/api/v1/",
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
));
// Interceptors
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// اضافه کردن JWT به همه درخواستها
final token = await TokenStorage.getAccessToken();
if (token != null) {
options.headers["Authorization"] = "Bearer $token";
}
handler.next(options);
},
onError: (error, handler) async {
// در صورت 401، token را refresh کن
if (error.response?.statusCode == 401) {
final refreshed = await _refreshToken();
if (refreshed) {
// درخواست را مجدد ارسال کن
final newRequest = error.requestOptions;
newRequest.headers["Authorization"] =
"Bearer ${await TokenStorage.getAccessToken()}";
final response = await _dio.fetch(newRequest);
handler.resolve(response);
return;
}
}
handler.next(error);
},
));
}
Future _refreshToken() async {
try {
final refresh = await TokenStorage.getRefreshToken();
final response = await Dio().post(
"https://api.shop.com/api/v1/auth/refresh/",
data: {"refresh": refresh},
);
await TokenStorage.saveAccessToken(response.data["access"]);
return true;
} catch (_) {
await TokenStorage.clear();
return false;
}
}
// GET
Future> getProducts({int page = 1, String? search}) async {
final response = await _dio.get(
"products/",
queryParameters: {
"page": page,
if (search != null) "search": search,
},
);
return (response.data["results"] as List)
.map((json) => Product.fromJson(json))
.toList();
}
// POST
Future createProduct(ProductCreate data) async {
final response = await _dio.post(
"products/",
data: data.toJson(),
);
return Product.fromJson(response.data);
}
// PUT
Future updateProduct(int id, ProductUpdate data) async {
final response = await _dio.put(
"products/$id/",
data: data.toJson(),
);
return Product.fromJson(response.data);
}
// DELETE
Future deleteProduct(int id) async {
await _dio.delete("products/$id/");
}
}
۷.۴ JSON Serialization
روش ۱: دستی (برای model های ساده)
class Product {
final int id;
final String name;
final double price;
final String? imageUrl;
Product({required this.id, required this.name, required this.price, this.imageUrl});
factory Product.fromJson(Map json) {
return Product(
id: json["id"] as int,
name: json["name"] as String,
price: (json["price"] as num).toDouble(),
imageUrl: json["image_url"] as String?,
);
}
Map toJson() => {
"id": id,
"name": name,
"price": price,
"image_url": imageUrl,
};
}
روش ۲: json_serializable (خودکار)
flutter pub add json_annotation
flutter pub add --dev json_serializable build_runner
// product.dart
import "package:json_annotation/json_annotation.dart";
part "product.g.dart";
@JsonSerializable()
class Product {
final int id;
final String name;
final double price;
@JsonKey(name: "image_url")
final String? imageUrl;
@JsonKey(name: "created_at")
final DateTime createdAt;
Product({
required this.id,
required this.name,
required this.price,
this.imageUrl,
required this.createdAt,
});
factory Product.fromJson(Map json) =>
_$ProductFromJson(json);
Map toJson() => _$ProductToJson(this);
}
// تولید کد:
// flutter pub run build_runner build
روش ۳: freezed (برای immutable + union types)
flutter pub add freezed_annotation
flutter pub add --dev freezed build_runner
import "package:freezed_annotation/freezed_annotation.dart";
part "product.freezed.dart";
part "product.g.dart";
@freezed
class Product with _$Product {
const factory Product({
required int id,
required String name,
required double price,
@JsonKey(name: "image_url") String? imageUrl,
}) = _Product;
factory Product.fromJson(Map json) =>
_$ProductFromJson(json);
}
// استفاده
final p = Product(id: 1, name: "لپتاپ", price: 50000000);
// copyWith خودکار
final updated = p.copyWith(price: 45000000);
// equals و hashCode خودکار
print(p == updated); // false
۷.۵ ادغام با Django REST Framework
سمت Django: مثال ساده
# products/serializers.py
from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id", "name", "price", "image_url", "created_at"]
# products/views.py
from rest_framework import viewsets, permissions
from rest_framework_simplejwt.authentication import JWTAuthentication
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
authentication_classes = [JWTAuthentication]
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [permissions.AllowAny()]
return [permissions.IsAuthenticated()]
# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("products", ProductViewSet)
urlpatterns = router.urls
# settings.py
INSTALLED_APPS = [..., "rest_framework", "rest_framework_simplejwt"]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
# CORS برای اتصال از Flutter
CORS_ALLOWED_ORIGINS = [
"http://localhost:8080", # Flutter web dev
]
CORS_ALLOW_CREDENTIALS = True
سمت Flutter: Repository Pattern
// repositories/product_repository.dart
class ProductRepository {
final ApiClient _api;
ProductRepository(this._api);
Future> getProducts({int page = 1}) async {
try {
final response = await _api.get("/products/", queryParams: {"page": page});
return (response["results"] as List)
.map((json) => Product.fromJson(json))
.toList();
} on DioException catch (e) {
throw _handleError(e);
}
}
Future getProduct(int id) async {
final response = await _api.get("/products/$id/");
return Product.fromJson(response);
}
Future createProduct(ProductCreate data) async {
final response = await _api.post("/products/", data: data.toJson());
return Product.fromJson(response);
}
Future deleteProduct(int id) async {
await _api.delete("/products/$id/");
}
AppException _handleError(DioException error) {
if (error.type == DioExceptionType.connectionTimeout) {
return NetworkException("اتصال timeout شد");
}
if (error.response?.statusCode == 404) {
return NotFoundException("پیدا نشد");
}
if (error.response?.statusCode == 401) {
return UnauthorizedException("نیاز به ورود");
}
if (error.response?.statusCode == 400) {
final errors = error.response?.data["errors"] ?? {};
return ValidationException(errors);
}
return AppException("خطای ناشناخته");
}
}
// Custom exceptions
class AppException implements Exception {
final String message;
AppException(this.message);
}
class NetworkException extends AppException {
NetworkException(super.message);
}
class NotFoundException extends AppException {
NotFoundException(super.message);
}
class UnauthorizedException extends AppException {
UnauthorizedException(super.message);
}
class ValidationException extends AppException {
final Map errors;
ValidationException(this.errors) : super("اطلاعات نامعتبر");
}
۷.۶ JWT Authentication
Login Flow کامل
// auth_repository.dart
class AuthRepository {
final Dio _dio;
final TokenStorage _storage;
AuthRepository(this._dio, this._storage);
Future login(String email, String password) async {
final response = await _dio.post(
"/auth/login/",
data: {"email": email, "password": password},
);
final data = response.data;
// ذخیره tokens
await _storage.saveAccessToken(data["access"]);
await _storage.saveRefreshToken(data["refresh"]);
return User.fromJson(data["user"]);
}
Future register(String email, String password, String name) async {
final response = await _dio.post(
"/auth/register/",
data: {
"email": email,
"password": password,
"name": name,
},
);
await _storage.saveAccessToken(response.data["access"]);
await _storage.saveRefreshToken(response.data["refresh"]);
return User.fromJson(response.data["user"]);
}
Future logout() async {
try {
await _dio.post("/auth/logout/");
} catch (_) {}
await _storage.clear();
}
Future getCurrentUser() async {
final token = await _storage.getAccessToken();
if (token == null) return null;
try {
final response = await _dio.get("/auth/me/");
return User.fromJson(response.data);
} catch (_) {
return null;
}
}
}
// در Riverpod
@riverpod
class Auth extends _$Auth {
@override
Future build() async {
return ref.read(authRepositoryProvider).getCurrentUser();
}
Future login(String email, String password) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return ref.read(authRepositoryProvider).login(email, password);
});
}
Future logout() async {
await ref.read(authRepositoryProvider).logout();
state = const AsyncValue.data(null);
}
}
// در Login Page
class LoginPage extends ConsumerStatefulWidget {
@override
ConsumerState createState() => _LoginPageState();
}
class _LoginPageState extends ConsumerState {
final _formKey = GlobalKey();
String email = "", password = "";
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
return Scaffold(
appBar: AppBar(title: const Text("ورود")),
body: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: "ایمیل"),
onChanged: (v) => email = v,
validator: (v) => v?.isEmpty ?? true ? "ایمیل لازم است" : null,
),
TextFormField(
decoration: const InputDecoration(labelText: "رمز"),
obscureText: true,
onChanged: (v) => password = v,
),
ElevatedButton(
onPressed: authState.isLoading ? null : () async {
if (!_formKey.currentState!.validate()) return;
await ref.read(authProvider.notifier).login(email, password);
if (mounted) context.go("/");
},
child: authState.isLoading
? const CircularProgressIndicator()
: const Text("ورود"),
),
],
),
),
);
}
}
۷.۷ File Upload
// با image_picker
import "package:image_picker/image_picker.dart";
Future uploadAvatar() async {
final picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1080,
);
if (image == null) return;
final formData = FormData.fromMap({
"avatar": await MultipartFile.fromFile(
image.path,
filename: "avatar.jpg",
),
});
final response = await _dio.post(
"/users/me/avatar/",
data: formData,
options: Options(
contentType: "multipart/form-data",
),
onSendProgress: (sent, total) {
final percent = (sent / total * 100).toStringAsFixed(0);
print("$percent% آپلود شد");
},
);
}
// چندین فایل
final formData = FormData.fromMap({
"title": "آلبوم من",
"images": await Future.wait(
images.map((file) => MultipartFile.fromFile(file.path)),
),
});
۷.۸ بهترین تجربیات
- Repository Pattern. business logic از API جدا.
- Custom Exceptions. error handling بهتر.
- Interceptors برای auth. در یک جا.
- Refresh token خودکار. در 401.
- Timeout مناسب. 10s connect، 30s receive.
- Cancel token. برای cancel کردن درخواست.
- Cache strategy. با dio_cache_interceptor.
- Retry با exponential backoff. برای errors موقت.
- JSON serialization با code generation.
- API key در .env. از flutter_dotenv استفاده کنید.
۷.۹ خلاصه فصل
- http برای کارهای ساده، Dio برای production
- JSON با dart:convert، json_serializable یا freezed
- ادغام با Django REST: ViewSet، JWT، CORS
- Repository Pattern برای business logic
- JWT با refresh token خودکار
- File upload با Multipart
- Error handling حرفهای
در فصل بعد: ذخیرهسازی محلی — SharedPreferences، Hive، sqflite، Isar.