Flutter’da Data Katmanı
Merhaba değerli okurlar. Daha önce hem clean architecture hemde feature first yapısına değinmiştim. Bu yazımda sizlere Feature First yapısındaki data katmanına değinip sizlere anlatmaya çalışacağım. Sözü uzatmadan konuya giriş yapıyorum.
Data Katmanı Nedir
Temiz Mimari’de (Clean Architecture) her katmanın bir sorumluluğu vardır.
- Presentation Katmanı: “Kullanıcıya ne göstereceğim ve kullanıcının dokunuşlarına nasıl tepki vereceğim?” diye sorar.
- Domain Katmanı: “Uygulamanın kuralları ne? Bir ‘Kullanıcı’ nedir? ‘Giriş yapma’ işlemi ne anlama gelir?” diye sorar. Bu katman, projenin beynidir ve dış dünyadan tamamen habersizdir. İnternet var mı, veritabanı mı kullanılıyor, umurunda değildir.
- Data Katmanı: “Domain katmanının istediği veriyi nereden ve nasıl getireceğim?” diye sorar. Bu katman, mimarinin “amele”sidir. Kirli işleri yapar: İnternete bağlanır, API’lerden veri çeker, veritabanına kayıt yapar, telefonun hafızasından veri okur.
Özetle Data Katmanı’nın görevi: Domain katmanının “Bana kullanıcı bilgilerini getir” komutunu alıp, bunu “Şu adresteki API’ye bir GET isteği at, gelen JSON’ı işle, eğer hata olursa bunu uygun bir şekilde bildir” gibi teknik adımlara dönüştürmektir.
Elbette bu görevi yerine getirirken data adında klasör açıp tüm dart dosyalarını paldır küldür içerisine tanımlamıyoruz. Görev dağılımı yapıp ilgili klasörler altında işlemler yapıyoruz.
data/datasources Klasörü: Veri Kaynakları
Uygulamamızın verileri farklı yerlerde olabilir. Bir kullanıcı profilini düşünelim:
- Kullanıcının adı, e-postası, profil resmi sunucuda (API) olabilir.
- Kullanıcının uygulamayı en son ne zaman açtığı bilgisi telefonun kendi hafızasında olabilir.
İşte bu farklı “yerlere” Veri Kaynağı (DataSource) diyoruz. Remote ve local olarak iki dosya şeklinde oluşturup veri kaynaklarını kontrol edebiliriz. Kısaca remote ve local data source nedir ne yapar onada bakalım
remote_data_source.dart (Uzak Veri Kaynağı):
- Uygulamanın fiziksel olarak dışında bulunan tüm veri kaynaklarıyla iletişim kuran sınıftır. %99 oranında bu, bir web sunucusuna (API) yapılan HTTP istekleri (GET, POST, PUT, DELETE) anlamına gelir.
- Amacı ve Sorumluluğu: Sadece ve sadece sunucu ile konuşmaktır. Görevi, “bana şu veriyi ver” veya “al bu veriyi kaydet” gibi ham istekleri sunucuya göndermek ve sunucudan gelen ham cevabı (genellikle JSON formatında) geri döndürmektir. Hata yönetimi de onun sorumluluğundadır. Eğer sunucu 404 Not Found (Bulunamadı) veya 500 Internal Server Error (Sunucu Hatası) gibi bir hata dönerse, bunu anlayan ve uygun bir “İstisna” (Exception) fırlatan yer burasıdır.
- Neden Gerekli? Tüm ağ (network) kodumuzu tek bir yerde toplarız. Yarın öbür gün http paketinden dio paketine geçmek istersek, sadece bu dosyayı değiştirmemiz yeterli olur. Uygulamanın geri kalanı bu değişiklikten etkilenmez.
Burada bir konuya değinmek istiyorum; Exception fırlatır dedik ama akıllara şu soru gelebilir core altında genellikle dartz ya da farklı metotlarla error handler yapısı kuruyoruz. Yani uygulama genelinde kullanılabilir hata yakalama yapısı kuruyoruz o zaman neden burası exception fırlatıyor? Burası aslında kurduğumuz error handler yapısını kullanarak bu hataları fırlatır yani try catch yapısı kurarak ayrı bir yönetim yapmıyoruz.
local_data_source.dart (Yerel Veri Kaynağı):
- Uygulamanın çalıştığı cihazın kendi depolama birimleriyle iletişim kuran sınıftır.
- Amacı ve Sorumluluğu: SharedPreferences (küçük anahtar-değer verileri için), Hive veya SQLite (daha karmaşık veritabanları için) gibi yerel depolama araçlarına veri yazmak ve okumaktır. Örneğin, kullanıcının en son seçtiği tema rengini kaydetmek veya çevrimdışı kullanım için verileri önbelleğe (cache) almak bu sınıfın görevidir.
- Neden Gerekli? Tıpkı remote gibi, tüm yerel depolama mantığını tek bir yerde toplar. Gelecekte SharedPreferences yerine Hive kullanmaya karar verirsek, sadece bu dosyayı değiştirmemiz gerekir.
data/models Klasörü: Veri Modelleri
Uygulamamıza gelen verilerden istediğimiz ölçülere sığan güzel ve yakışıklıları ayırt ettiğ… şaka şaka konunun öyle bir şeyle alakası yok arkadaşlar.
DataSource’lar bize ham veri getirir (genellikle JSON). Bu veriyi Dart dilinde anlamlı bir nesneye dönüştürmemiz gerekir. İşte bu noktada Model’ler devreye girer.
user_model.dart (Örnek Model):
- Bir veri kaynağından (özellikle API’den) gelen verinin birebir kopyası olan bir Dart sınıfıdır.
- Amacı ve Sorumluluğu: API’den gelen JSON verisinin yapısını birebir yansıtmaktır. En önemli görevi, gelen JSON’ı alıp kendini oluşturan bir metoda (fromJson) sahip olmaktır. Ayrıca, nesneyi tekrar JSON’a çevirmek için bir toJson metodu da içerebilir. Bu modeller, API’nin “kirli” detaylarını içerebilir. Örneğin API, kullanıcı adını user_name olarak gönderiyorsa, modeldeki değişken adı da userName olur ve fromJson metodu bu dönüşümü bilir.
- Neden Gerekli? (ÇOK ÖNEMLİ): Bu modeller, Domain katmanındaki Entity’lerden farklıdır.
- Model: Dış dünyanın (API) dilini konuşur. API değişirse, Model değişir.
- Entity (Domain katmanında): Uygulamanın kendi iş mantığının dilini konuşur. Temizdir, sadece iş kurallarının gerektirdiği veriyi tutar. fromJson gibi detayları bilmez.
Bu ayrım, API’de yapılacak bir değişikliğin (örneğin user_name alanının full_name olarak değişmesi) projenin kalbi olan Domain katmanını etkilemesini engeller. Değişikliği sadece UserModel içinde yönetiriz.
Modeller kısmını iyi anlamak için Flutter’da json işlemlerinin nasıl ve neden gerçekleştiğinide bilmek gerek. Bilmeyenler varsa araştırsın lütfen. Henüz o konuda ki yazım bitmedi, bitince birde benden dinlersiniz…
data/repositories Klasörü: Depo Uygulamaları
Şimdi en kritik parçaya geldik: Repository Implementation (Depo Uygulaması).
auth_repository_impl.dart (Örnek Depo Uygulaması):
- Bu sınıf, Domain katmanında tanımlanmış olan soyut AuthRepository sözleşmesini (contract/interface) hayata geçiren, yani uygulayan somut sınıftır.
- Amacı ve Sorumluluğu: Bu sınıf bir orkestra şefi gibidir. Domain katmanından gelen “kullanıcı girişi yap” isteğini alır ve bu işi yapmak için doğru adımları atar:
- Karar Verme: “Bu veriyi nereden almalıyım? Önce yerel önbelleğe (local data source) mi bakayım, orada yoksa sunucuya (remote data source) mı sorayım?” gibi kararları verir.
- Veri Kaynaklarını Koordine Etme: RemoteDataSource’u veya LocalDataSource’u çağırır.
- Hata Yönetimi: DataSource’lardan gelebilecek teknik “İstisnaları” (ServerException, CacheException) yakalar. Bu teknik hataları, uygulamanın geri kalanının anlayabileceği daha basit, kullanıcı dostu “Başarısızlıklara” (ServerFailure, CacheFailure) dönüştürür. Bu, “Sunucuya bağlanılamadı” gibi bir mesajı kullanıcıya göstermemizi sağlar.
- Model’den Entity’ye Dönüşüm: DataSource’dan gelen UserModel’i alır ve Domain katmanının istediği saf User Entity’sine dönüştürür. Genellikle UserModel sınıfı, User Entity’sini extends (genişletir) ettiği için bu dönüşüm otomatik olabilir.
- Neden Gerekli? Bu sınıf, Domain katmanı ile Data katmanı arasında bir köprü ve bir maskedir. Domain katmanı, verinin internetten mi, veritabanından mı, yoksa bir güvercinin ayağından mı geldiğini bilmez ve bilmemelidir. Sadece “Bana veriyi ver” der. RepositoryImpl ise bu isteği yerine getirirken tüm karmaşık ve kirli detayları (ağ kontrolü, hata yakalama, veri kaynağı seçimi) Domain’den gizler. Buna Soyutlama (Abstraction) denir.
Buraya kadar kavramları öğrendik birde örnek senaryo üzerinden kodları görelim arkadaşlar. Kodları görmek umarım hafızanızda daha kalıcı yer edinmesini sağlar bu bilgilerin..
Senaryomuz şu; Kullanıcı, e-posta ve şifre ile giriş yapacak.
Domain Katmanındaki Sözleşme (Hatırlatma)
Data katmanının ne yapacağını söyleyen sözleşme Domain katmanında şöyle görünür:
// lib/features/auth/domain/repositories/auth_repository.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
import '../entities/user.dart';
// Bu bir sözleşmedir. Sadece NE yapılacağını söyler, NASIL yapılacağını söylemez.
abstract class AuthRepository {
Future<Either<Failure, User>> login(String email, String password);
}
// Either<Failure, User>: Bu işlem ya bir Failure (hata) ya da
// bir User (başarılı veri) döndürecek demektir.Model Sınıfını Oluşturmak (data/models)
API’den şöyle bir JSON geldiğini varsayalım:
{
“user_id”: 123,
“email_address”: “test@test.com”,
“name”: “Ali Veli”,
“auth_token”: “xyz123abc”
}Bunu alıp bir model haline getirmemiz gerekiyorki dart dilinde bu verilerin bir anlamı olsun.
// lib/features/auth/data/models/user_model.dart
import '../../domain/entities/user.dart';
// UserModel, API'den gelen verinin birebir kopyasıdır.
// Aynı zamanda bir User'dır (extends sayesinde). Bu, dönüşümü kolaylaştırır.
class UserModel extends User {
UserModel({
required int id,
required String email,
required String name,
required String token,
}) : super(id: id, email: email, name: name, token: token);
// Bu fabrika metodu, JSON'ı alıp bir UserModel nesnesine dönüştürür.
// API'nin "kirli" isimlerini (user_id, email_address) bilir ve temizler.
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['user_id'],
email: json['email_address'],
name: json['name'],
token: json['auth_token'],
);
}
// Bu metod, nesneyi tekrar JSON'a çevirir.
Map<String, dynamic> toJson() {
return {
'user_id': id,
'email_address': email,
'name': name,
'auth_token': token,
};
}
}Veri Kaynağını Oluşturmak (data/datasources)
// lib/features/auth/data/datasources/auth_remote_data_source.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../../core/error/exceptions.dart';
import '../models/user_model.dart';
// Bu, uzak veri kaynağının yapması gereken işlerin sözleşmesidir.
abstract class AuthRemoteDataSource {
Future<UserModel> login(String email, String password);
}
// Bu da sözleşmenin somut uygulamasıdır.
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final http.Client client;
AuthRemoteDataSourceImpl({required this.client});
@override
Future<UserModel> login(String email, String password) async {
final response = await client.post(
Uri.parse('https://api.example.com/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'email': email, 'password': password}),
);
// Başarılı ise (HTTP 200), JSON'ı parse et ve UserModel döndür.
if (response.statusCode == 200) {
return UserModel.fromJson(json.decode(response.body));
} else {
// Başarısız ise, teknik bir istisna fırlat.
// Bu hatayı Repository katmanı yakalayıp işleyecek.
throw ServerException();
}
}
}Repository Uygulamasını Oluşturmak (data/repositories)
Bu son parça, her şeyi bir araya getirir.
// lib/features/auth/data/repositories/auth_repository_impl.dart
import 'package:dartz/dartz.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/error/failure.dart';
import '../../../../core/network/network_info.dart'; // İnternet bağlantısını kontrol eden yardımcı
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_data_source.dart';
// Bu sınıf, Domain'deki AuthRepository sözleşmesini UYGULAR (implements).
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo; // Bağımlılık olarak enjekte edilir
AuthRepositoryImpl({
required this.remoteDataSource,
required this.networkInfo,
});
// Sözleşmedeki login metodunu burada hayata geçiriyoruz.
@override
Future<Either<Failure, User>> login(String email, String password) async {
// 1. Karar Verme: İnternet var mı?
if (await networkInfo.isConnected) {
try {
// 2. Veri Kaynağını Koordine Etme: RemoteDataSource'u çağır.
final remoteUser = await remoteDataSource.login(email, password);
// 3. Dönüşüm: Gelen UserModel, aynı zamanda bir User'dır.
// Başarılı sonucu `Right` ile sarmalayarak döndür.
return Right(remoteUser);
} on ServerException {
// 4. Hata Yönetimi: DataSource'dan gelen teknik hatayı yakala.
// Kullanıcı dostu bir Failure'a dönüştür ve `Left` ile sarmala.
return Left(ServerFailure());
}
} else {
// İnternet yoksa, doğrudan bir hata döndür.
return Left(NetworkFailure());
}
}
}Buraya kadar olan tüm akışı özetleyecek olursak;
Repository (AuthRepositoryImpl) çağrılır.
- İlk olarak internet bağlantısını NetworkInfo ile kontrol eder.
- İnternet varsa, AuthRemoteDataSource’un login metodunu çağırır.
- DataSource, http paketi ile API’ye isteği gönderir.
- API başarılı cevap (200 OK) dönerse, DataSource gelen JSON’ı UserModel.fromJson ile bir UserModel nesnesine çevirir ve bunu Repository’e geri verir.
- API hata (401, 500 vb.) dönerse, DataSource bir ServerException fırlatır.
- Repository, bu ServerException’ı try-catch bloğu ile yakalar.
- Sonuç:
- Başarılıysa: Repository, aldığı UserModel’i (ki bu aynı zamanda bir User’dır) Right(userModel) olarak sarmalayıp Domain katmanına sunar.
- Başarısızsa: Repository, yakaladığı Exception’ı bir Left(ServerFailure())’a dönüştürerek Domain katmanına sunar.
Bu yapı sayesinde, Domain katmanı hiçbir zaman http, JSON, ServerException gibi teknik detaylarla uğraşmak zorunda kalmaz. Sadece “Başarılı mı oldu? Veriyi ver. Başarısız mı oldu? Hatayı ver.” şeklinde basit ve temiz bir iletişim kurar.
Değerli arkadaşlar bu yazımda sizlere Data katmanından bahsetmeye çalıştım umarım sizler için faydalı olmuştur…
Sonraki yazılarımda görüşmek üzere..
Github: www.github.com/abdullah017
Linkedin: www.linkedin.com/in/abdullahtas
Stackoverflow: https://stackoverflow.com/users/13807726/abdullah-t
#FREEPALESTINA
