Flutter API Integration: REST Servislerle Sağlam Entegrasyon

14 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Flutter API integrationFlutter REST APIFlutter repository patternDio FlutterFlutter error handlingFlutter network layerFlutter clean architecture apiFlutter backend integration

# Flutter API Entegrasyonu: Güvenilir REST Servis Mimarisi

Sağlam bir API katmanı, kullanıcı deneyimini doğrudan etkiler. Geliştirdiğim her prodüksiyon Flutter uygulamasında API katmanı en kritik altyapı bileşenidir. Yanlış kurgulanırsa rastgele hatalar, tutarsız state'ler ve sinirli kullanıcılarla uğraşırsınız. Doğru kurgulanırsa üzerine eklediğiniz her özellik sorunsuz çalışır.

Bu yazıda her projede başvurduğum mimariyi baştan sona anlatacağım: Dio yapılandırması, interceptor'lar, repository pattern, Result pattern ile hata yönetimi, offline önbellekleme ve takımlarda defalarca gördüğüm yaygın hatalar.

Neden Dio?

Flutter'ın dahili `http` paketi prototip için yeterlidir, ancak prodüksiyon uygulamalarında interceptor, cancel token, form-data yüklemesi ve detaylı timeout kontrolü gerekir. Dio bunların hepsini kutudan çıkar çıkmaz sunar.

dart
import class="code-string">'package:dio/dio.dart';

class DioClient {
  late final Dio _dio;

  DioClient({required String baseUrl, String? authToken}) {
    _dio = Dio(
      BaseOptions(
        baseUrl: baseUrl,
        connectTimeout: const Duration(seconds: class="code-number">10),
        receiveTimeout: const Duration(seconds: class="code-number">15),
        sendTimeout: const Duration(seconds: class="code-number">10),
        headers: {
          class="code-string">'Content-Type': class="code-string">'application/json',
          class="code-string">'Accept': class="code-string">'application/json',
          if (authToken != null) class="code-string">'Authorization': class="code-string">'Bearer $authToken',
        },
      ),
    );

    _dio.interceptors.addAll([
      LoggingInterceptor(),
      AuthInterceptor(),
      RetryInterceptor(dio: _dio),
    ]);
  }

  Dio get dio => _dio;
}

Burada dikkat edilmesi gereken birkaç nokta var. Üç timeout değerini de her zaman açıkça belirtiyorum. Dio'nun varsayılan değerleri oldukça cömert ve mobil ağlarda kullanıcıyı dönen bir spinner'a baktırmaktansa hızlı hata vermek daha iyidir. Auth token'ı yapıcıda enjekte edip interceptor üzerinden yeniliyorum, bunu birazdan göstereceğim.

Interceptor'lar

Dio'nun asıl gücü interceptor'larda yatıyor. Prodüksiyon uygulamalarında her zaman üç şey için interceptor kuruyorum: loglama, token yenileme ve tekrar deneme mantığı.

Loglama Interceptor'ı

dart
class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final method = options.method.toUpperCase();
    final url = options.uri.toString();
    debugPrint(class="code-string">'→ $method $url');
    if (options.data != null) {
      debugPrint(class="code-string">'  Body: ${options.data}');
    }
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final code = response.statusCode;
    final url = response.requestOptions.uri.toString();
    debugPrint(class="code-string">'← $code $url');
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    debugPrint(class="code-string">'✗ ${err.type}: ${err.message}');
    handler.next(err);
  }
}

Token Yenileme ile Auth Interceptor

En sık kullandığım interceptor budur. 401 yanıtı geldiğinde token yenileme dener ve orijinal isteği tam olarak bir kez tekrar gönderir. Yenileme kendisi başarısız olursa kullanıcıyı çıkış yaptırır.

dart
class AuthInterceptor extends Interceptor {
  final TokenStorage _tokenStorage;
  final AuthService _authService;
  bool _isRefreshing = false;

  AuthInterceptor({
    required TokenStorage tokenStorage,
    required AuthService authService,
  })  : _tokenStorage = tokenStorage,
        _authService = authService;

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    final token = await _tokenStorage.getAccessToken();
    if (token != null) {
      options.headers[class="code-string">'Authorization'] = class="code-string">'Bearer $token';
    }
    handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode != class="code-number">401 || _isRefreshing) {
      return handler.next(err);
    }

    _isRefreshing = true;
    try {
      final refreshToken = await _tokenStorage.getRefreshToken();
      if (refreshToken == null) throw Exception(class="code-string">'Refresh token yok');

      final newTokens = await _authService.refreshToken(refreshToken);
      await _tokenStorage.saveTokens(newTokens);

      class=class="code-string">"code-comment">// Yeni token ile orijinal isteği tekrar gönder
      final options = err.requestOptions;
      options.headers[class="code-string">'Authorization'] = class="code-string">'Bearer ${newTokens.accessToken}';
      final response = await Dio().fetch(options);
      handler.resolve(response);
    } catch (_) {
      await _tokenStorage.clear();
      handler.reject(err);
    } finally {
      _isRefreshing = false;
    }
  }
}

Tekrar Deneme Interceptor'ı

Mobil ağ koşulları tahmin edilemez. Basit bir üstel geri çekilme (exponential backoff) ile tekrar deneme mekanizması birçok kullanıcı şikayetini ortadan kaldırır.

dart
class RetryInterceptor extends Interceptor {
  final Dio dio;
  final int maxRetries;

  RetryInterceptor({required this.dio, this.maxRetries = class="code-number">2});

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    final isRetryable = err.type == DioExceptionType.connectionTimeout ||
        err.type == DioExceptionType.receiveTimeout ||
        err.type == DioExceptionType.connectionError;

    if (!isRetryable) return handler.next(err);

    int attempt = err.requestOptions.extra[class="code-string">'retryCount'] ?? class="code-number">0;
    if (attempt >= maxRetries) return handler.next(err);

    attempt++;
    err.requestOptions.extra[class="code-string">'retryCount'] = attempt;

    final delay = Duration(milliseconds: class="code-number">300 * (class="code-number">1 << attempt));
    await Future.delayed(delay);

    try {
      final response = await dio.fetch(err.requestOptions);
      handler.resolve(response);
    } on DioException catch (e) {
      handler.next(e);
    }
  }
}

Tam API Katmanı Mimarisi

Kullandığım mimari Clean Architecture prensiplerini takip eder, ancak uygulamada pratikliği azaltacak kadar aşırı soyutlama yapmaz. Genel görünüm şöyledir:

UI (Widget'lar)
  ↓
BLoC / Cubit / Riverpod
  ↓
Repository (soyutlamalar)
  ↓
Remote Data Source ←→ Local Data Source
  ↓                        ↓
Dio (HTTP)             Hive / Drift / SharedPrefs

Data Transfer Object'ler (DTO'lar)

DTO'lar API yanıtlarıyla birebir eşleşir. API'nin JSON yapısını asla domain katmanınıza sızdırmayın.

dart
class UserDto {
  final int id;
  final String userName;
  final String emailAddress;
  final String? avatarUrl;
  final String createdAt;

  UserDto({
    required this.id,
    required this.userName,
    required this.emailAddress,
    this.avatarUrl,
    required this.createdAt,
  });

  factory UserDto.fromJson(Map<String, dynamic> json) => UserDto(
        id: json[class="code-string">'id'] as int,
        userName: json[class="code-string">'user_name'] as String,
        emailAddress: json[class="code-string">'email_address'] as String,
        avatarUrl: json[class="code-string">'avatar_url'] as String?,
        createdAt: json[class="code-string">'created_at'] as String,
      );

  Map<String, dynamic> toJson() => {
        class="code-string">'id': id,
        class="code-string">'user_name': userName,
        class="code-string">'email_address': emailAddress,
        class="code-string">'avatar_url': avatarUrl,
        class="code-string">'created_at': createdAt,
      };

  User toDomain() => User(
        id: id,
        name: userName,
        email: emailAddress,
        avatarUrl: avatarUrl,
        memberSince: DateTime.parse(createdAt),
      );
}

Domain Modeli

dart
class User {
  final int id;
  final String name;
  final String email;
  final String? avatarUrl;
  final DateTime memberSince;

  const User({
    required this.id,
    required this.name,
    required this.email,
    this.avatarUrl,
    required this.memberSince,
  });
}

Remote Data Source

dart
abstract class UserRemoteDataSource {
  Future<UserDto> getUser(int id);
  Future<List<UserDto>> getUsers({int page = class="code-number">1, int limit = class="code-number">20});
  Future<UserDto> updateUser(int id, Map<String, dynamic> data);
}

class UserRemoteDataSourceImpl implements UserRemoteDataSource {
  final Dio _dio;

  UserRemoteDataSourceImpl(this._dio);

  @override
  Future<UserDto> getUser(int id) async {
    final response = await _dio.get(class="code-string">'/users/$id');
    return UserDto.fromJson(response.data);
  }

  @override
  Future<List<UserDto>> getUsers({int page = class="code-number">1, int limit = class="code-number">20}) async {
    final response = await _dio.get(
      class="code-string">'/users',
      queryParameters: {class="code-string">'page': page, class="code-string">'limit': limit},
    );
    final list = response.data[class="code-string">'data'] as List;
    return list.map((e) => UserDto.fromJson(e)).toList();
  }

  @override
  Future<UserDto> updateUser(int id, Map<String, dynamic> data) async {
    final response = await _dio.put(class="code-string">'/users/$id', data: data);
    return UserDto.fromJson(response.data);
  }
}

Repository

Repository, domain katmanı ile data katmanı arasındaki geçit kapısıdır. Ağdan mı yoksa önbellekten mi veri çekileceğine karar verir ve DTO'ları domain modellerine dönüştürür.

dart
abstract class UserRepository {
  Future<Result<User>> getUser(int id);
  Future<Result<List<User>>> getUsers({int page = class="code-number">1});
  Future<Result<User>> updateUser(int id, Map<String, dynamic> data);
}

class UserRepositoryImpl implements UserRepository {
  final UserRemoteDataSource _remoteDataSource;
  final UserLocalDataSource _localDataSource;

  UserRepositoryImpl({
    required UserRemoteDataSource remoteDataSource,
    required UserLocalDataSource localDataSource,
  })  : _remoteDataSource = remoteDataSource,
        _localDataSource = localDataSource;

  @override
  Future<Result<User>> getUser(int id) async {
    try {
      final dto = await _remoteDataSource.getUser(id);
      await _localDataSource.cacheUser(dto);
      return Result.success(dto.toDomain());
    } on DioException catch (e) {
      class=class="code-string">"code-comment">// Ağ hatasında önbelleği dene
      final cached = await _localDataSource.getCachedUser(id);
      if (cached != null) return Result.success(cached.toDomain());
      return Result.failure(NetworkFailure.fromDioException(e));
    } catch (e) {
      return Result.failure(UnexpectedFailure(e.toString()));
    }
  }

  @override
  Future<Result<List<User>>> getUsers({int page = class="code-number">1}) async {
    try {
      final dtos = await _remoteDataSource.getUsers(page: page);
      return Result.success(dtos.map((d) => d.toDomain()).toList());
    } on DioException catch (e) {
      return Result.failure(NetworkFailure.fromDioException(e));
    }
  }

  @override
  Future<Result<User>> updateUser(int id, Map<String, dynamic> data) async {
    try {
      final dto = await _remoteDataSource.updateUser(id, data);
      await _localDataSource.cacheUser(dto);
      return Result.success(dto.toDomain());
    } on DioException catch (e) {
      return Result.failure(NetworkFailure.fromDioException(e));
    }
  }
}

Hata Yönetimi Stratejisi

Exception'ları katmanlar arası fırlatmak, unutulmuş catch blokları ve prodüksiyonda çökmeler demektir. Bunun yerine, çağıran tarafı hem başarılı hem başarısız senaryoları açıkça ele almaya zorlayan bir `Result` tipi kullanıyorum.

Result Tipi

dart
sealed class Result<T> {
  const Result();

  factory Result.success(T data) = Success<T>;
  factory Result.failure(Failure failure) = Err<T>;

  R when<R>({
    required R Function(T data) success,
    required R Function(Failure failure) failure,
  });
}

class Success<T> extends Result<T> {
  final T data;
  const Success(this.data);

  @override
  R when<R>({
    required R Function(T data) success,
    required R Function(Failure failure) failure,
  }) => success(data);
}

class Err<T> extends Result<T> {
  final Failure failure;
  const Err(this.failure);

  @override
  R when<R>({
    required R Function(T data) success,
    required R Function(Failure failure) failure,
  }) => failure(this.failure);
}

Tipli Hatalar

dart
sealed class Failure {
  final String message;
  const Failure(this.message);
}

class NetworkFailure extends Failure {
  final int? statusCode;

  const NetworkFailure(super.message, {this.statusCode});

  factory NetworkFailure.fromDioException(DioException e) {
    return switch (e.type) {
      DioExceptionType.connectionTimeout =>
        const NetworkFailure(class="code-string">'Bağlantı zaman aşımına uğradı'),
      DioExceptionType.receiveTimeout =>
        const NetworkFailure(class="code-string">'Sunucu yanıt vermekte gecikti'),
      DioExceptionType.badResponse => NetworkFailure(
          e.response?.data?[class="code-string">'message'] ?? class="code-string">'Sunucu hatası',
          statusCode: e.response?.statusCode,
        ),
      DioExceptionType.connectionError =>
        const NetworkFailure(class="code-string">'İnternet bağlantısı yok'),
      _ => NetworkFailure(class="code-string">'Ağ hatası: ${e.message}'),
    };
  }
}

class CacheFailure extends Failure {
  const CacheFailure(super.message);
}

class UnexpectedFailure extends Failure {
  const UnexpectedFailure(super.message);
}

Result'ın UI Katmanında Kullanımı

dart
class UserCubit extends Cubit<UserState> {
  final UserRepository _repository;

  UserCubit(this._repository) : super(const UserState.loading());

  Future<void> loadUser(int id) async {
    emit(const UserState.loading());
    final result = await _repository.getUser(id);
    result.when(
      success: (user) => emit(UserState.loaded(user)),
      failure: (failure) => emit(UserState.error(failure.message)),
    );
  }
}

Bu pattern, yakalanmamış exception olasılığını tamamen ortadan kaldırır. Her çağrı noktası hatayla ilgilenmek zorundadır ve derleyici size yardım eder.

Offline Destek ve Önbellekleme

Mobil kullanıcılar sürekli bağlantı kaybeder. İyi bir API katmanı, hata ekranları göstermek yerine zarif bir şekilde degrade olur.

Hive ile Local Data Source

dart
abstract class UserLocalDataSource {
  Future<void> cacheUser(UserDto user);
  Future<UserDto?> getCachedUser(int id);
  Future<void> cacheUsers(List<UserDto> users);
  Future<List<UserDto>> getCachedUsers();
  Future<void> clearCache();
}

class UserLocalDataSourceImpl implements UserLocalDataSource {
  final Box<Map> _userBox;

  UserLocalDataSourceImpl(this._userBox);

  @override
  Future<void> cacheUser(UserDto user) async {
    await _userBox.put(user.id.toString(), user.toJson());
  }

  @override
  Future<UserDto?> getCachedUser(int id) async {
    final json = _userBox.get(id.toString());
    if (json == null) return null;
    return UserDto.fromJson(Map<String, dynamic>.from(json));
  }

  @override
  Future<void> cacheUsers(List<UserDto> users) async {
    final entries = {for (var u in users) u.id.toString(): u.toJson()};
    await _userBox.putAll(entries);
  }

  @override
  Future<List<UserDto>> getCachedUsers() async {
    return _userBox.values
        .map((e) => UserDto.fromJson(Map<String, dynamic>.from(e)))
        .toList();
  }

  @override
  Future<void> clearCache() async => await _userBox.clear();
}

Cache-First Stratejisi

Pratikte, sık değişmeyen veriler (kullanıcı profilleri, ayarlar) için cache-first, diğer her şey için network-first pattern kullanıyorum. Hangi stratejinin uygulanacağına repository karar verir:

dart
Future<Result<User>> getUser(int id) async {
  class=class="code-string">"code-comment">// Önce önbelleği dene
  final cached = await _localDataSource.getCachedUser(id);
  if (cached != null) {
    class=class="code-string">"code-comment">// Önbellekten hemen döndür, arka planda güncelle
    _refreshUserInBackground(id);
    return Result.success(cached.toDomain());
  }

  class=class="code-string">"code-comment">// Önbellekte yok, ağdan çekmek gerekiyor
  try {
    final dto = await _remoteDataSource.getUser(id);
    await _localDataSource.cacheUser(dto);
    return Result.success(dto.toDomain());
  } on DioException catch (e) {
    return Result.failure(NetworkFailure.fromDioException(e));
  }
}

void _refreshUserInBackground(int id) async {
  try {
    final dto = await _remoteDataSource.getUser(id);
    await _localDataSource.cacheUser(dto);
  } catch (_) {
    class=class="code-string">"code-comment">// Sessiz hata — önbellekteki veri hala geçerli
  }
}

Bağlantı Durumu Takibi

Önbelleklemeyi bir connectivity dinleyicisiyle eşleştiriyorum. Böylece UI çevrimdışı banner gösterebilir ve gereksiz ağ çağrılarından kaçınılır:

dart
class ConnectivityService {
  final Connectivity _connectivity = Connectivity();

  Stream<bool> get onlineStream =>
      _connectivity.onConnectivityChanged.map(
        (result) => result != ConnectivityResult.none,
      );

  Future<bool> get isOnline async {
    final result = await _connectivity.checkConnectivity();
    return result != ConnectivityResult.none;
  }
}

JSON Serileştirme

Basit bir projenin ötesinde her zaman `json_serializable` ile `freezed` kullanıyorum. Pratik bir kurulum:

dart
class=class="code-string">"code-comment">// pubspec.yaml dependencies
class=class="code-string">"code-comment">// json_annotation, freezed_annotation
class=class="code-string">"code-comment">// dev_dependencies: json_serializable, freezed, build_runner

@freezed
class ApiResponse<T> with _$ApiResponse<T> {
  const factory ApiResponse({
    required bool success,
    required T data,
    String? message,
    ApiMeta? meta,
  }) = _ApiResponse;

  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Object?) fromJsonT,
  ) => _$ApiResponseFromJson(json, fromJsonT);
}

@freezed
class ApiMeta with _$ApiMeta {
  const factory ApiMeta({
    required int currentPage,
    required int lastPage,
    required int perPage,
    required int total,
  }) = _ApiMeta;

  factory ApiMeta.fromJson(Map<String, dynamic> json) =>
      _$ApiMetaFromJson(json);
}

Kod üretimi için:

bash
dart run build_runner build --delete-conflicting-outputs

Yaygın API Entegrasyon Hataları

Bunlar, incelediğim veya devraldığım kod tabanlarında defalarca gördüğüm sorunlardır.

1. Widget İçinden Doğrudan API Çağrısı

En yaygın hata budur. Dio'yu doğrudan widget içinden çağırdığınızda test edilebilirliği, yeniden kullanılabilirliği ve gelecekte veri kaynağını değiştirme imkanını kaybedersiniz.

dart
class=class="code-string">"code-comment">// Kötü: Widget içinde API çağrısı
class UserScreen extends StatefulWidget { ... }
class _UserScreenState extends State<UserScreen> {
  Future<void> _loadUser() async {
    final response = await Dio().get(class="code-string">'https:class=class="code-string">"code-comment">//api.example.com/users/class="code-number">1');
    setState(() { _user = User.fromJson(response.data); });
  }
}

class=class="code-string">"code-comment">// İyi: Repository üzerinden geçin
class _UserScreenState extends State<UserScreen> {
  Future<void> _loadUser() async {
    final result = await userRepository.getUser(class="code-number">1);
    result.when(
      success: (user) => setState(() { _user = user; }),
      failure: (f) => _showError(f.message),
    );
  }
}

2. İptal Etmeyi Göz Ardı Etmek

Kullanıcı bir istek devam ederken sayfadan ayrılırsa, isteği iptal etmelisiniz. Aksi takdirde bant genişliği israf edersiniz ve dispose edilmiş bir widget üzerinde `setState` çağırma riski doğar.

dart
class _UserScreenState extends State<UserScreen> {
  final CancelToken _cancelToken = CancelToken();

  Future<void> _loadUser() async {
    final response = await dio.get(
      class="code-string">'/users/class="code-number">1',
      cancelToken: _cancelToken,
    );
    class=class="code-string">"code-comment">// ...
  }

  @override
  void dispose() {
    _cancelToken.cancel(class="code-string">'Widget dispose edildi');
    super.dispose();
  }
}

3. Sayfalama Eksikliği

Geliştirme sırasında 10 öğeyle tüm kayıtları tek seferde çekmek işe yarar. Prodüksiyonda 10.000 kayıtla çöker. Baştan cursor veya offset tabanlı sayfalama uygulayın.

dart
class PaginatedResult<T> {
  final List<T> items;
  final int currentPage;
  final int totalPages;
  final bool hasMore;

  PaginatedResult({
    required this.items,
    required this.currentPage,
    required this.totalPages,
  }) : hasMore = currentPage < totalPages;
}

4. Base URL'leri Sabit Kodlamak

Ortama özgü yapılandırma kullanın. Ben genelde flavor'a göre base URL tanımlarım:

dart
enum Environment { dev, staging, prod }

class AppConfig {
  final String baseUrl;
  final Duration timeout;

  const AppConfig._({required this.baseUrl, required this.timeout});

  factory AppConfig.fromEnvironment(Environment env) {
    return switch (env) {
      Environment.dev => const AppConfig._(
          baseUrl: class="code-string">'https:class=class="code-string">"code-comment">//dev-api.example.com',
          timeout: Duration(seconds: class="code-number">30),
        ),
      Environment.staging => const AppConfig._(
          baseUrl: class="code-string">'https:class=class="code-string">"code-comment">//staging-api.example.com',
          timeout: Duration(seconds: class="code-number">15),
        ),
      Environment.prod => const AppConfig._(
          baseUrl: class="code-string">'https:class=class="code-string">"code-comment">//api.example.com',
          timeout: Duration(seconds: class="code-number">10),
        ),
    };
  }
}

5. Hataları Sessizce Yutmak

Boş catch blokları hata ayıklamanın düşmanıdır. En azından hatayı loglayın. Daha iyisi, Result pattern kullanarak hataları her zaman yüzeye çıkarın.

dart
class=class="code-string">"code-comment">// Berbat: sessiz hata
try {
  await dio.post(class="code-string">'/orders', data: order.toJson());
} catch (_) {}

class=class="code-string">"code-comment">// Daha iyi: tipli hata olarak yay
try {
  await dio.post(class="code-string">'/orders', data: order.toJson());
} on DioException catch (e) {
  return Result.failure(NetworkFailure.fromDioException(e));
}

API Katmanını Test Etmek

Katmanlı bir API mimarisi kolayca test edilebilir. Data source'u mock'layın, repository mantığını izole olarak test edin.

dart
class MockUserRemoteDataSource extends Mock
    implements UserRemoteDataSource {}

void main() {
  late UserRepositoryImpl repository;
  late MockUserRemoteDataSource mockRemote;
  late MockUserLocalDataSource mockLocal;

  setUp(() {
    mockRemote = MockUserRemoteDataSource();
    mockLocal = MockUserLocalDataSource();
    repository = UserRepositoryImpl(
      remoteDataSource: mockRemote,
      localDataSource: mockLocal,
    );
  });

  test(class="code-string">'uzak kaynaktan kullanıcıyı getirir ve önbelleğe alır', () async {
    final dto = UserDto(id: class="code-number">1, userName: class="code-string">'Ada', ...);
    when(() => mockRemote.getUser(class="code-number">1)).thenAnswer((_) async => dto);
    when(() => mockLocal.cacheUser(dto)).thenAnswer((_) async {});

    final result = await repository.getUser(class="code-number">1);

    expect(result, isA<Success<User>>());
    verify(() => mockLocal.cacheUser(dto)).called(class="code-number">1);
  });

  test(class="code-string">'ağ hatasında önbelleğe düşer', () async {
    final dto = UserDto(id: class="code-number">1, userName: class="code-string">'Ada', ...);
    when(() => mockRemote.getUser(class="code-number">1)).thenThrow(
      DioException(type: DioExceptionType.connectionError, ...),
    );
    when(() => mockLocal.getCachedUser(class="code-number">1)).thenAnswer((_) async => dto);

    final result = await repository.getUser(class="code-number">1);

    expect(result, isA<Success<User>>());
  });
}

Sonuç

Temiz bir API katmanı, güvenilir her Flutter uygulamasının temelidir. Bu yazıda anlattığım mimari -- interceptor'lı Dio, repository pattern, Result ile tipli hata yönetimi ve offline önbellekleme -- onlarca prodüksiyon projesinde bana iyi hizmet etti. Ham HTTP çağrılarına kıyasla başlangıçta biraz daha fazla kurulum gerektirir, ancak bakım kolaylığı, test edilebilirlik ve kullanıcı deneyimi açısından getirisi çok büyüktür.

İsterseniz mevcut API katmanınızı birlikte inceleyip refactor edebiliriz.

İlgili Makaleler

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç