~/icsd.ir — bash
SYSTEM_ONLINE

شبکه و 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> createProduct(Map data) async {
  final response = await http.post(
    Uri.parse("https://api.example.com/products/"),
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer $token",
    },
    body: jsonEncode(data),
  );
  
  return jsonDecode(response.body);
}

// با timeout
final response = await http.get(url).timeout(
  const Duration(seconds: 10),
  onTimeout: () => throw TimeoutException("Request timed out"),
);
    

۷.۳ 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)),
  ),
});
    

۷.۸ بهترین تجربیات

  1. Repository Pattern. business logic از API جدا.
  2. Custom Exceptions. error handling بهتر.
  3. Interceptors برای auth. در یک جا.
  4. Refresh token خودکار. در 401.
  5. Timeout مناسب. 10s connect، 30s receive.
  6. Cancel token. برای cancel کردن درخواست.
  7. Cache strategy. با dio_cache_interceptor.
  8. Retry با exponential backoff. برای errors موقت.
  9. JSON serialization با code generation.
  10. 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.

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.