Flutter'da REST API Entegrasyonu: Dio ve HTTP Paketi

Gerçek dünya Flutter uygulamalarının büyük çoğunluğu bir backend API'siyle iletişim kurar. Bu yazıda önce temel http paketini, ardından daha güçlü dio paketini ve production kalitesinde API katmanı oluşturmayı ele alacağız.

http Paketi ile Temel Kullanım

// pubspec.yaml: http: ^1.2.0

import 'package:http/http.dart' as http;
import 'dart:convert';

class ApiServis {
  static const _baseUrl = "https://jsonplaceholder.typicode.com";

  Future<List<Post>> postlariGetir() async {
    final response = await http.get(
      Uri.parse("/posts"),
      headers: {"Content-Type": "application/json"},
    );

    if (response.statusCode == 200) {
      final List data = jsonDecode(response.body);
      return data.map((j) => Post.fromJson(j)).toList();
    }
    throw Exception("API hatası: ");
  }

  Future<Post> postOlustur(String baslik, String icerik) async {
    final response = await http.post(
      Uri.parse("/posts"),
      headers: {"Content-Type": "application/json"},
      body: jsonEncode({"title": baslik, "body": icerik, "userId": 1}),
    );

    if (response.statusCode == 201) return Post.fromJson(jsonDecode(response.body));
    throw Exception("Oluşturma hatası: ");
  }
}

JSON Model Sınıfı

class Post {
  final int id;
  final String baslik;
  final String icerik;

  const Post({required this.id, required this.baslik, required this.icerik});

  factory Post.fromJson(Map<String, dynamic> json) => Post(
    id: json['id'] as int,
    baslik: json['title'] as String,
    icerik: json['body'] as String,
  );

  Map<String, dynamic> toJson() => {
    'id': id,
    'title': baslik,
    'body': icerik,
  };
}

Dio ile Gelişmiş Kullanım

// pubspec.yaml: dio: ^5.4.3

import 'package:dio/dio.dart';

class DioIstemci {
  late final Dio _dio;

  DioIstemci() {
    _dio = Dio(BaseOptions(
      baseUrl: "https://api.example.com",
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 30),
      headers: {"Accept": "application/json"},
    ));

    _dio.interceptors.addAll([
      _AuthInterceptor(),
      _LoggingInterceptor(),
      _RetryInterceptor(_dio),
    ]);
  }

  Future<T> get<T>(
    String path,
    T Function(dynamic) fromJson, {
    Map<String, dynamic>? queryParams,
  }) async {
    try {
      final response = await _dio.get(path, queryParameters: queryParams);
      return fromJson(response.data);
    } on DioException catch (e) {
      throw _hatayaDonustur(e);
    }
  }

  Exception _hatayaDonustur(DioException e) => switch (e.type) {
    DioExceptionType.connectionTimeout   => const TimeoutException("Bağlantı zaman aşımı"),
    DioExceptionType.receiveTimeout      => const TimeoutException("Yanıt zaman aşımı"),
    DioExceptionType.badResponse         => ApiException(e.response?.statusCode ?? 0, e.message ?? ""),
    DioExceptionType.connectionError     => const NetworkException("İnternet bağlantısı yok"),
    _ => Exception("Bilinmeyen hata: "),
  };
}

Auth Interceptor

class _AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final token = await TokenDepo().getToken();
    if (token != null) {
      options.headers["Authorization"] = "Bearer ";
    }
    handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      // Token yenile, isteği tekrarla
      final yeniToken = await AuthServis().tokenYenile();
      err.requestOptions.headers["Authorization"] = "Bearer ";
      final response = await Dio().fetch(err.requestOptions);
      handler.resolve(response);
      return;
    }
    handler.next(err);
  }
}

Retry Interceptor

class _RetryInterceptor extends Interceptor {
  final Dio dio;
  _RetryInterceptor(this.dio);

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    final extra = err.requestOptions.extra;
    final retryCount = extra['retryCount'] as int? ?? 0;

    if (retryCount < 3 && _yenilenebilir(err)) {
      extra['retryCount'] = retryCount + 1;
      await Future.delayed(Duration(seconds: retryCount + 1));
      final response = await dio.fetch(err.requestOptions);
      handler.resolve(response);
      return;
    }
    handler.next(err);
  }

  bool _yenilenebilir(DioException e) =>
      e.type == DioExceptionType.connectionError ||
      e.response?.statusCode == 503;
}

FutureBuilder ile UI

class PostListesi extends StatelessWidget {
  const PostListesi({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<Post>>(
      future: ApiServis().postlariGetir(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        if (snapshot.hasError) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.error_outline, size: 64, color: Colors.red),
                const SizedBox(height: 16),
                Text("Hata: "),
                ElevatedButton(
                  onPressed: () => (context as Element).markNeedsBuild(),
                  child: const Text("Tekrar Dene"),
                ),
              ],
            ),
          );
        }
        final postlar = snapshot.data!;
        return ListView.builder(
          itemCount: postlar.length,
          itemBuilder: (context, i) => ListTile(
            title: Text(postlar[i].baslik),
            subtitle: Text(postlar[i].icerik, maxLines: 2, overflow: TextOverflow.ellipsis),
          ),
        );
      },
    );
  }
}

Dosya Yükleme

Future<String> dosyaYukle(String dosyaYolu) async {
  final formData = FormData.fromMap({
    "dosya": await MultipartFile.fromFile(
      dosyaYolu,
      filename: "resim.jpg",
      contentType: DioMediaType("image", "jpeg"),
    ),
    "aciklama": "Profil fotoğrafı",
  });

  final response = await _dio.post(
    "/upload",
    data: formData,
    onSendProgress: (sent, total) {
      final yuzde = (sent / total * 100).toStringAsFixed(1);
      print("Yükleniyor: %");
    },
  );

  return response.data["url"] as String;
}

Sonuç

Küçük projeler için http paketi yeterlidir. Production uygulamalarında Dio'nun interceptor sistemi, retry mekanizması ve zengin konfigürasyon seçenekleri büyük avantaj sağlar. Interceptor'ları servis katmanından bağımsız tutarak test edilebilirliği koruyun.