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.