flutter_generic_response_handle

Flutter’da Generic Response ile Etkili İstek Yönetimi

Generic Response Kavramının Tanıtımı

Flutter’da uygulama geliştirirken, API istekleriyle etkileşim kurmak oldukça yaygındır. Bu isteklerin sonuçlarını yönetmek ve kullanıcıya doğru bilgiyi sunmak, uygulamanın başarısı açısından kritik öneme sahiptir. İşte bu noktada Generic Response yapısı devreye girer. Generic Response, API isteklerinin sonucunu kapsülleyerek, başarı veya hata durumlarını standart bir formatta yönetmenizi sağlar.

İstek Yönetiminde Generic Response Kullanmanın Avantajları

  • Kod Tekrarını Azaltma: Generic Response yapısı, tekrar eden hata ve başarı durumlarını yönetmenizi kolaylaştırır.
  • Okunabilirlik: Kodun okunabilirliğini artırır ve hata ayıklamayı kolaylaştırır.
  • Genel Hata Yönetimi: Tüm istekler için merkezi bir hata yönetim mekanizması sunar.
  • Esneklik: İsteklerin sonuçlarını farklı tiplerde (String, List, Map vb.) yönetmenizi sağlar.

Generic Response Yapısının Oluşturulması

Generic Response yapısını oluşturmak için GenericResponse adında bir sınıf tanımlarız. Bu sınıf, isteklerin sonucunu, hata mesajını ve isteğin başarılı olup olmadığını içerecek şekilde yapılandırılır.

class GenericResponse<T> {
  final T? data;
  final String? errorMessage;
  final bool isSuccessful;

  GenericResponse(this.data, this.errorMessage, {this.isSuccessful = false});
}

Örnek olarak GenericResponse sınıfının implemente edilmesi

class GenericResponse<T> {
  final T? data;
  final String? errorMessage;
  final bool isSuccessful;

  GenericResponse(this.data, this.errorMessage, {this.isSuccessful = false});
  
  factory GenericResponse.success(T data) {
    return GenericResponse(data, null, isSuccessful: true);
  }

  factory GenericResponse.error(String errorMessage) {
    return GenericResponse(null, errorMessage, isSuccessful: false);
  }
}

API İsteklerinde Generic Response Kullanımı

Flutter’da HTTP istekleri yapmak için çeşitli kütüphaneler bulunmaktadır. Bu kütüphaneler arasında en popüler olanlardan biri Dio’dur. Dio, esnek ve güçlü bir HTTP istemcisidir. Bu yazıda yapılan istekler Dio kütüphanesi ile gerçekleştirilecektir. Bu kütüphaneyi kullanabilmek için öncelikle pubspec.yaml dosyamızdan Dio paketini bağımlılık olarak ekleyelim.

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.2
  provider: ^6.1.2

Dio ile GET İsteği

import 'package:dio/dio.dart';

Future<GenericResponse<List<String>>> fetchItems() async {
  try {
    final response = await Dio().get('https://example.com/items');
    if (response.statusCode == 200) {
      List<String> items = List<String>.from(response.data);
      return GenericResponse.success(items);
    } else {
      return GenericResponse.error('Failed to fetch items: ${response.statusCode}');
    }
  } catch (e) {
    return GenericResponse.error('An error occurred: ${e.toString()}');
  }
}

Yukarıdaki kod bloğundan görüleceği üzere dönüş tipimizi List<String> yerine GenericResponse ile sarmalayıp GenericResponse<List<String>> haline getiriyoruz. Bunun sebebi olası yaşanacak bir hata durumunda fonksiyonun dönüş değerinde bu fonksiyonu çağıran kısma hatayı iletebilmek. Eğer bunu GenericResponse ile iletmeyip throw ile fırlatsaydık bu sefer bu fonksiyonu çağırdığımız yerde tekrar try-catch bloğu ile hatayı yönetmek durumda kalacaktık. Her yerde try-catch bloğu kullanmak istemiyoruz çünkü hem kod okunabilirliğini azaltıyor hem de performans açısından daha kötü bir çözüm yolu.

Hata Yönetimi ve ErrorHandler Sınıfının Oluşturulması

API isteklerinde hata yönetimini daha etkin bir şekilde yapmak için ErrorHandler sınıfı oluşturabiliriz.

class ErrorHandler {
  static String handle(dynamic error) {
    if (error is DioError) {
      switch (error.type) {
        case DioErrorType.connectTimeout:
          return 'Connection Timeout';
        case DioErrorType.sendTimeout:
          return 'Send Timeout';
        case DioErrorType.receiveTimeout:
          return 'Receive Timeout';
        case DioErrorType.response:
          return 'Received invalid status code: ${error.response?.statusCode}';
        case DioErrorType.cancel:
          return 'Request to API server was cancelled';
        case DioErrorType.other:
          return 'Connection to API server failed due to internet connection';
      }
    }
    return 'Unexpected error occurred';
  }
}

Buradaki amacımız catch bloğunda yakaladığımız hatanın hangi türden kaynaklı olduğunu tespit ederek kolay bir şekilde uygun hata tipini döndürmek. Daha detaylı hata yönetimi için bu blog postunu inceleyebilirsiniz: https://medium.com/@mohammadjoumani/error-handling-in-flutter-a1dfe81a2e0

ViewModel Katmanında Generic Response Kullanımı

ViewModel, veri katmanı ile UI katmanı arasındaki bağlantıyı sağlar. Burada API çağrılarını yönetir ve UI’ya gerekli verileri sunar.

Örnek ViewModel

class ItemsViewModel extends ChangeNotifier {
  List<String> items = [];
  bool isLoading = false;

  Future<void> loadItems() async {
    isLoading = true;
    notifyListeners();

    final response = await fetchItems();
    if (response.isSuccessful) {
      items = response.data!;
    } else {
      // Hata mesajını yönet
    }

    isLoading = false;
    notifyListeners();
  }
}

UI Katmanında Generic Response Kullanımı

Widget’larda Generic Response Verilerinin İşlenmesi

UI katmanında, ViewModel’den gelen verileri kullanarak kullanıcı arayüzünü güncelleriz.

class ItemsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ItemsViewModel(),
      child: Consumer<ItemsViewModel>(
        builder: (context, viewModel, child) {
          if (viewModel.isLoading) {
            return Center(child: CircularProgressIndicator());
          } else if (viewModel.items.isEmpty) {
            return Center(child: Text('No items available'));
          } else {
            return ListView.builder(
              itemCount: viewModel.items.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(viewModel.items[index]));
              },
            );
          }
        },
      ),
    );
  }
}

Kullanıcı Arayüzünde Hata ve Başarı Durumlarının Gösterilmesi

API çağrılarının sonuçlarını kullanıcıya göstermek için SnackBar veya Dialog gibi bileşenler kullanabiliriz.

void showSnackBar(BuildContext context, String message, {bool isError = false}) {
  final snackBar = SnackBar(
    content: Text(message),
    backgroundColor: isError ? Colors.red : Colors.green,
  );
  ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

Best Practices

Kod Tekrarını Önleme Teknikleri

Kod tekrarını önlemek için ortak işlevsellikleri yardımcı sınıflar veya yöntemler haline getirmek faydalı olacaktır.

Generic Response Yapısının Genişletilebilirliği

Generic Response yapısını ihtiyaçlara göre genişletmek ve özelleştirmek mümkündür. Örneğin, metaData gibi ek alanlar eklenebilir.

Unit Test Yazımı ve Test Edilebilirlik

Unit test yazımı, kodun doğru çalıştığını doğrulamak için önemlidir. Generic Response yapısı, test yazımını kolaylaştırır.

import 'package:flutter_test/flutter_test.dart';

void main() {
  test('GenericResponse success case', () {
    final response = GenericResponse.success('Data loaded');
    expect(response.isSuccessful, true);
    expect(response.data, 'Data loaded');
    expect(response.errorMessage, null);
  });

  test('GenericResponse error case', () {
    final response = GenericResponse.error('Failed to load data');
    expect(response.isSuccessful, false);
    expect(response.data, null);
    expect(response.errorMessage, 'Failed to load data');
  });
}

Daha İleri Seviye Konular

Interceptor Kullanımı

Interceptor’lar, API istekleri ve yanıtlarını yakalayıp işlemek için kullanılır.

class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('Request: ${options.method} ${options.path}');
    super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('Response: ${response.statusCode} ${response.data}');
    super.onResponse(response, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print('Error: ${err.message}');
    super.onError(err, handler);
  }
}

Caching Mekanizmaları

Caching, API yanıtlarını saklayarak uygulamanın performansını artırabilir. Caching mekanizmalarını uygulamak için çeşitli stratejiler ve kütüphaneler kullanılabilir.

class CacheManager {
  final Map<String, dynamic> _cache = {};

  dynamic get(String key) => _cache[key];

  void set(String key, dynamic value) {
    _cache[key] = value;
  }
  
  Future<GenericResponse<List>> fetchItemsWithCache() async {
    final cacheManager = CacheManager();
    final cacheKey = ‘items’;
    
    if (cacheManager.get(cacheKey) != null) {
      return GenericResponse.success(List.from(cacheManager.get(cacheKey)));
    }
    
    try {
      final response = await Dio().get("https://example.com/items");
      if (response.statusCode == 200) {
        List items = List.from(response.data);
        cacheManager.set(cacheKey, items);
        return GenericResponse.success(items);
      } else {
        return GenericResponse.error(‘Failed to fetch items: ${response.statusCode}’);
      }
    }catch (e) {
        return GenericResponse.error(‘An error occurred: ${e.toString()}’);
    }
  }
}

Retry Mekanizmaları

Retry mekanizmaları, başarısız olan istekleri yeniden denemek için kullanılır. Bu, özellikle ağ sorunları veya geçici sunucu hataları durumunda yararlıdır.

class RetryInterceptor extends Interceptor {
  final int maxRetries;
  final int retryDelay;

  RetryInterceptor({this.maxRetries = 3, this.retryDelay = 1000});

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    var retryCount = 0;
    while (retryCount < maxRetries && shouldRetry(err)) {
      retryCount++;
      await Future.delayed(Duration(milliseconds: retryDelay));
      try {
        final response = await Dio().request(
          err.requestOptions.path,
          options: err.requestOptions,
        );
        handler.resolve(response);
        return;
      } catch (e) {
        continue;
      }
    }
    handler.next(err);
  }

  bool shouldRetry(DioError err) {
    return err.type == DioErrorType.other ||
        err.type == DioErrorType.connectTimeout ||
        err.type == DioErrorType.receiveTimeout;
  }
}

final dio = Dio();
dio.interceptors.add(RetryInterceptor());

Kavramların bir araya getirilerek oluşturulmuş bir uygulamayı aşağıda bulabilirsiniz. Direkt olarak kopyalarak sizlerde deneyebilirsiniz. Konuyu daha iyi kavrayabilmek için kendiniz yazmanızı da tavsiye ediyorum. Ayrıca burada kullanılan provider paketini pubspec.yaml dosyanıza eklemeyi unutmayın.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ItemsPage(),
    );
  }
}

void main() {
  runApp(MyApp());
}

class ItemsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ItemsViewModel(),
      child: Consumer<ItemsViewModel>(
        builder: (context, viewModel, child) {
          if (viewModel.isLoading) {
            return Center(child: CircularProgressIndicator());
          } else if (viewModel.items.isEmpty) {
            return Center(child: Text('No items available'));
          } else {
            return ListView.builder(
              itemCount: viewModel.items.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(viewModel.items[index]));
              },
            );
          }
        },
      ),
    );
  }
}

class ItemsViewModel extends ChangeNotifier {
  List<String> items = [];
  bool isLoading = false;

  ItemsViewModel() {
    loadItems();
  }

  Future<void> loadItems() async {
    isLoading = true;
    notifyListeners();

    final response = await fetchItemsWithCache();
    if (response.isSuccessful) {
      items = response.data!;
    } else {
      showSnackBar(response.errorMessage!);
    }

    isLoading = false;
    notifyListeners();
  }

  void showSnackBar(String message, {bool isError = false}) {
    // Snackbar gösterme kodu
  }
}

Future<GenericResponse<List<String>>> fetchItemsWithCache() async {
  final cacheManager = CacheManager();
  final cacheKey = 'items';

  if (cacheManager.get(cacheKey) != null) {
    return GenericResponse.success(List<String>.from(cacheManager.get(cacheKey)));
  }

  try {
    final response = await Dio().get('https://example.com/items');
    if (response.statusCode == 200) {
      List<String> items = List<String>.from(response.data);
      cacheManager.set(cacheKey, items);
      return GenericResponse.success(items);
    } else {
      return GenericResponse.error('Failed to fetch items: ${response.statusCode}');
    }
  } catch (e) {
    return GenericResponse.error('An error occurred: ${e.toString()}');
  }
}

Generic Response Yaklaşımının Genel Değerlendirmesi

Generic Response, Flutter uygulamalarında API isteklerini yönetmenin etkili bir yoludur. Hem hata hem de başarı durumlarını standart bir şekilde ele alarak kodun okunabilirliğini ve sürdürülebilirliğini artırır. Böylece daha az kodla esnek bir yapı sağlayabilir aynı zamanda uygulamanıza performansta katabilirsiniz. Bu makale, Flutter uygulamalarında Generic Response yapısını kullanarak istek yönetimini detaylı bir şekilde ele alarak, geliştiricilere rehberlik etmeyi amaçlamaktadır. Uygulama geliştirirken, bu yaklaşımları kullanarak kodunuzun daha sürdürülebilir, test edilebilir ve yönetilebilir olmasını sağlayabilirsiniz. Okuduğunuz için teşekkür eder esenlikler dilerim.

Leave a Comment

Your email address will not be published. Required fields are marked *