Flutter Uygulama Mimarisi: Repository Pattern 💫 🌌 ✨
Published: 2022-01-27 21:42:00

Not: Bu yazı [Andrea Bizzotto](https://codewithandrea.com/articles/flutter-repository-pattern/#keep-it-simple)'nun makalesinin Türkçe çevirisi niteliğini taşır. Orijinal içeriğe linkten ulaşabilirsiniz.

Tasarım kalıpları, yazılım tasarımında sık karşılaşılan sorunları çözmeye yardımcı olan kullanışlı şablonlardır.

Uygulama mimarisi (app architecture) söz konusu olduğunda, yapısal (structural) tasarım kalıpları, uygulamanın farklı bölümlerinin nasıl düzenlendiğine karar vermemize yardımcı olabilir.

Bu bağlamda,  backend API gibi çeşitli kaynaklardan veri nesnelerine (data objects) erişmek ve bunları uygulamanın domain katmanına (iş mantığımızın yaşadığı yer olan) type-safe (tür güvenli) varlıklar olarak kullanılabilir hale getirmek için repository pattern'ı kullanabiliriz.

Ve bu makalede repository pattern'ı ayrıntılı olarak öğreneceğiz.
• Nedir ve ne zaman kullanılır?
• Bazı pratik örnekler
• Abstract (soyut) ve concrete (somut) sınıfları


## Repository Design Pattern'ı Nedir?

Bunu anlamak için aşağıdaki mimari diyagramı ele alalım:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d0o5ys1w0x8vmi6snde5.png)

Veri katmanında (data layer) repositori'ler bulunur. Ve onların işi:

• Domain modellerini (ya da entities) veri katmanındaki (data layer) veri kaynaklarının uygulama ayrıntılarından ayırmak,

• Veri aktarım nesnelerini (data transfer objects), domain katmanı tarafından anlaşılan doğrulanmış varlıklara (entities) dönüştürmek,

• (isteğe bağlı olarak)  data caching (veri önbelleğe alma) gibi işlemleri gerçekleştirmektir.

_Yukarıdaki şema, uygulamanızın mimarisini oluşturmanın birçok olası yolundan yalnızca birini göstermektedir. MVC, MVVM veya Clean Architecture gibi farklı bir mimariyi takip ederseniz işler farklı görünecektir, ancak aynı kavramlar geçerlidir._


Ayrıca widget'ların , iş mantığı (business logic) veya ağ koduyla hiçbir ilgisi olmayan sunum katmanına (presentation layer) nasıl ait olduğuna da dikkat edin.

_Widget'larınız doğrudan bir REST API'sinden veya uzak bir veritabanından key-value çiftleriyle çalışıyorsa, yanlış yapıyorsunuz demektir. Başka bir deyişle: iş mantığını (business logic) UI kodunuzla karıştırmayın. Bu, kodunuzu test etmeyi, hata ayıklamayı ve nedenini bulmayı çok daha zor hale getirecektir._

## Repository Pattern Ne Zaman Kullanılır?

Uygulamanızın, uygulamanın geri kalanından izole etmek istediğiniz yapılandırılmamış verileri (unstructured data) (JSON gibi) döndüren birçok farklı uç noktaya sahip karmaşık bir veri katmanı varsa, repository pattern çok kullanışlıdır.

Daha genel olarak,  repository pattern'in en uygun olduğu birkaç kullanım örneği:

• REST API'leri ile çalışmak
• Yerel(local) veya uzak(remote) veritabanlarıyla çalışmak (örn. Sembast, Hive, Firestore, vb.)
• Cihaza özel API'lerle çalışmak(ör. izinler, kamera, konum vb.)

Bu yaklaşımın büyük bir yararı, kullandığınız herhangi bir 3. taraf API'sinde son derece önemli değişiklikler olması durumunda, yalnızca repository kodunuzu güncellemeniz gerekmesidir.

Öyleyse onları nasıl kullanacağımızı görelim! 🚀

Örnek olarak, [OpenWeatherMap](https://openweathermap.org/api) API'sinden hava durumu verilerini çeken basit bir Flutter uygulamasını inceleyeceğiz.

[API belgelerini](https://openweathermap.org/current) okuyarak, JSON formatındaki bazı yanıt verileri örnekleriyle birlikte API'yi nasıl çağıracağımızı öğrenebiliriz.


Repository pattern, tüm ağ ve JSON serileştirme kodunu soyutlamak için harikadır.

Örneğin, repository için interface tanımlayan abstract(soyut) bir sınıf:

```
abstract class WeatherRepository {
  Future<Weather> getWeather({required String city});
}

```

WeatherRepository'nin yalnızca bir yöntemi vardır, ancak daha fazlası da olabilir (örneğin, tüm CRUD işlemlerini desteklemek istiyorsanız).


Önemli olan, repository'nin belirli bir şehir için hava durumunu nasıl alacağımıza dair bir sözleşme tanımlamamıza izin vermesidir.

[http ](https://pub.dev/packages/http) veya [dio ](https://pub.dev/packages/dio) gibi bir ağ istemcisi (networking client) kullanarak gerekli API çağrılarını yapan somut (concrete) bir sınıfla `WeatherRepository`'i implemente ederiz.


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

class HttpWeatherRepository implements WeatherRepository {
  HttpWeatherRepository({required this.api, required this.client});
  // tüm API ayrıntılarını tanımlayan özel sınıf
  final OpenWeatherMapAPI api;
  // API'ye çağrı yapmak için bir client
  final http.Client client;

  // yöntemi soyut sınıfta uygular
  Future<Weather> getWeather({required String city}) {
    // TODO: istek gönder, yanıtı ayrıştır, Hava durumu nesnesini döndür veya hata at
  }
}

```

Tüm bu uygulama ayrıntıları, veri katmanıyla (data layer) ilgili endişelerdir ve uygulamanın geri kalanı bunları umursamamalı, hatta bilmemelidir.

## JSON verilerini ayrıştırma

Elbette , API yanıt verilerini (response data) ayrıştırmak için JSON serileştirme koduyla birlikte bir `Weather` model sınıfı (veya entity ) tanımlamamız gerekecek:

```
class Weather {
  factory Weather.fromJson(Map<String, dynamic> json) {
    // TODO: JSON'u ayrıştır ve doğrulanmış Hava Durumu nesnesini döndür
  }
}

```

JSON yanıtı birçok farklı alan içerebilirken, yalnızca kullanıcı arayüzünde (UI) kullanılacak olanları ayrıştırmamız gerektiğini unutmayın.


JSON ayrıştırma kodunu elle yazabilir veya [Freezed ](https://pub.dev/packages/freezed) gibi bir kod oluşturma paketi kullanabiliriz . JSON serileştirme hakkında daha fazla bilgi edinmek için [Freezed Kullanarak Flutter'da JSON Nasıl Ayrıştırılır'a ](https://dev.to/gulsenkeskin/freezed-kullanarak-flutterda-json-nasil-ayristirilir-10m6) bakabilirsiniz.


## Repository Initializing

Bir repository tanımladıktan sonra, onu başlatmanın ve uygulamanın geri kalanı için erişilebilir hale getirmenin bir yoluna ihtiyacımız var.

Bunu yapmak için kullanılan sözdizimi, seçtiğiniz DI/state management (durum yönettimi) çözümünüze bağlı olarak değişir.

İşte [get_it ](https://pub.dev/packages/get_it) kullanan bir örnek :

```
import 'package:get_it/get_it.dart';

GetIt.instance.registerLazySingleton<WeatherRepository>(
  () => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);
```

İşte [Riverpod](https://pub.dev/packages/riverpod) paketinden bir sağlayıcı kullanan başka biri:

```
import 'package:flutter_riverpod/flutter_riverpod.dart';

final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
  return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});

```

[Flutter_bloc](https://pub.dev/packages/flutter_bloc) paketine dahilseniz , işte eşdeğeri :

```
import 'package:flutter_bloc/flutter_bloc.dart';

RepositoryProvider<WeatherRepository>(
  create: (_) => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client()),
  child: MyApp(),
))
```

Sonuç aynıdır: Repository'nizi bir kez başlattığınızda, ona uygulamanızın herhangi bir yerinden (widget'lar, bloklar, conroller vb.) erişebilirsiniz.


## Abstract (soyut) ve concrete(somut) sınıflar

Repository oluştururken sık sorulan bir soru şudur: gerçekten abstract bir sınıfa ihtiyacınız var mı , yoksa sadece concrete bir sınıf oluşturup tüm işinizi halledebilir misiniz?

İki sınıfa giderek daha fazla yöntem eklemek oldukça sıkıcı olabileceğinden, bu çok geçerli bir endişedir:

```
abstract class WeatherRepository {
  Future<Weather> getWeather({required String city});
  Future<Forecast> getHourlyForecast({required String city});
  Future<Forecast> getDailyForecast({required String city});
}

class HttpWeatherRepository implements WeatherRepository {
  HttpWeatherRepository({required this.api, required this.client});
  // tüm API ayrıntılarını tanımlayan özel sınıf
  final OpenWeatherMapAPI api;
  // API'ye çağrı yapmak için client
  final http.Client client;

  Future<Weather> getWeather({required String city}) { ... }
  Future<Forecast> getHourlyForecast({required String city}) { ... }
  Future<Forecast> getDailyForecast({required String city}) { ... }
}

```

Yazılım tasarımında sıklıkla olduğu gibi, cevap şudur:

Öyleyse, her yaklaşımın bazı artılarına ve eksilerine bakalım

## Abstract (soyut) sınıfları kullanma:

• Repository'mizin arayüzünü karışıklık olmadan tek bir yerden görebiliriz.

• Repository'i tamamen farklı bir uygulama ile değiştirebiliriz. 
(örneğin `DioWeatherRepository `yerine `HttpWeatherRepository`) 

• Daha fazla ortak kod.


## Yalnızca concrete (somut) sınıfları kullanma:

• Daha az ortak kod

• "jump to reference" (referansa atla), respository methodları yalnızca bir sınıfta bulunacağı için çalışır.

• Repository adını değiştirirsek farklı bir uygulamaya geçmek daha fazla değişiklik gerektirir (ancak VSCode ile tüm projedeki isimleri yeniden adlandırmak kolaydır).

Hangi yaklaşımı kullanacağımıza karar verirken, kodumuz için nasıl test yazacağımızı da bulmalıyız.


## Repositoriy'lerle test yazma:

Abstract (soyut) sınıflar burada bize herhangi bir avantaj sağlamaz, çünkü Dart'ta tüm sınıflar örtük bir arayüze sahiptir (implicit interface) .


Bu, şunu yapabileceğimiz anlamına gelir:


```
// not: Dart'ta her zaman somut bir sınıf uygulayabiliriz
class FakeWeatherRepository implements HttpWeatherRepository {

  // hemen bir değer döndüren sahte bir uygulama
  Future<Weather> getWeather({required String city}) { 
    return Future.value(Weather(...));
  }
}

```

Başka bir deyişle, testlerde repository'lerimize hazır yanıtlar vermek istiyorsak, abstract(soyut) sınıflar oluşturmaya gerek yoktur.

Aslında, [mocktail](https://pub.dev/packages/mocktail) gibi paketler bunu kendi avantajlarına kullanır ve biz de onları şu şekilde kullanabiliriz:

```
import 'package:mocktail/mocktail.dart';

class MockWeatherRepository extends Mock implements HttpWeatherRepository {}

final mockWeatherRepository = MockWeatherRepository();
when(() => mockWeatherRepository.getWeather('London'))
          .thenAnswer((_) => Future.value(Weather(...)));
```

Testlerinizi yazarken, depolarınıza yukarıda yaptığımız gibi hazır yanıtlar verebilirsiniz.

Ancak başka bir seçenek daha var ve bu, temel alınan veri kaynağıyla ilişkilidir.

`HttpWeatherRepository'nin` Nasıl tanımlandığını hatırlayalım :

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

class HttpWeatherRepository implements WeatherRepository {
  HttpWeatherRepository({required this.api, required this.client});
  // tüm API ayrıntılarını tanımlayan özel sınıf
  final OpenWeatherMapAPI api;
  // API'ye çağrı yapmak için istemci
  final http.Client client;

  // yöntemi soyut sınıfta uygular
  Future<Weather> getWeather({required String city}) {
    // TODO: istek gönder, yanıtı ayrıştır, Hava durumu nesnesini döndür veya hata at
  }
}
```

Bu durumda, `HttpWeatherRepository` constructor'ına iletilen `http.Client` nesnesini taklit etmeyi seçebiliriz. Bunu nasıl yapabileceğinizi gösteren örnek bir test:


```
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';

class MockHttpClient extends Mock implements http.Client {}

void main() {
  test('repository with mocked http client', () async {
    // setup
    final mockHttpClient = MockHttpClient();
    final api = OpenWeatherMapAPI();
    final weatherRepository =
        HttpWeatherRepository(api: api, client: mockHttpClient);
    when(() => mockHttpClient.get(api.weather('London')))
        .thenAnswer((_) => Future.value(/* some valid http.Response */));
    // run
    final weather = await weatherRepository.getWeather(city: 'London');
    // verify
    expect(weather, Weather(...));
  });
}
```

Repository'lerin nasıl test edileceğini bulduktan sonra, abstract sınıflar hakkındaki ilk sorumuza geri dönelim.


## Repository'ler abstract bir sınıfa ihtiyaç duymayabilir

Genel olarak, aynı arayüze uyan birçok uygulamaya ihtiyacınız varsa , abstract bir sınıf oluşturmak mantıklıdır.

Örneğin, hem `StatelessWidget` hem de `StatefulWidget` Flutter SDK'da abstract sınıflardır , çünkü alt sınıflara ayrılmaları amaçlanır .


Ancak repository'ler ile çalışırken, belirli bir repository için muhtemelen yalnızca bir implementasyona ihtiyacınız olacaktır.



_Belirli bir repository için yalnızca bir implementasyona ihtiyacınız olacak ve bunu tek, abstract(somut) bir sınıf olarak tanımlayabilirsiniz._


## En Düşük Ortak Payda

Her şeyi bir arayüzün arkasına koymak, sizi farklı yeteneklere sahip API'ler arasında en düşük ortak paydayı seçmeye itebilir.

Bir API veya backend, Akış tabanlı (Stream-based) bir API ile modellenebilen gerçek zamanlı (realtime) güncellemeleri destekler.

Ancak saf REST kullanıyorsanız (websocketler olmadan), yalnızca bir istek gönderebilir ve Future-based bir API ile en iyi modellenen tek bir yanıt (single response) alabilirsiniz.

Bununla başa çıkmak oldukça kolaydır: sadece stream-based bir API kullanın ve REST kullanıyorsanız tek değerli bir akış döndürün.

Ancak bazen daha geniş API farklılıkları olabilir.

Örneğin, Firestore [transaction'ları ve toplu yazmaları (batched writes) ](https://firebase.google.com/docs/firestore/manage-data/transactions) destekler. Bu tür API'ler , genel bir arabirimin arkasında kolayca soyutlanamayacak şekilde, builder pattern'ı  kullanır.

## Repository'ler yatay olarak ölçeklenir

Uygulamanız büyüdükçe, belirli bir repository'e giderek daha fazla method eklediğinizi görebilirsiniz.

Bu, backend büyük bir API yüzeyine sahipse veya uygulamanız birçok farklı veri kaynağına (data source) bağlanırsa gerçekleşebilir.


Bu senaryoda, ilgili yöntemleri bir arada tutarak birden çok repository oluşturmayı düşünün. Örneğin, bir e-Ticaret uygulaması oluşturuyorsanız, ürün listeleme, alışveriş sepeti, sipariş yönetimi, kimlik doğrulama, ödeme vb. için ayrı repository'leriniz olabilir.


## Basit tutun

Her zamanki gibi, işleri basit tutmak her zaman iyi bir fikirdir. Bu nedenle, API'lerinizi fazla düşünmekten kendinizi alıkoymayın.

## Çözüm

_Veri katmanınızın (data layer) tüm uygulama ayrıntılarını (örn. JSON serileştirme) gizlemek için repository modelini kullanın. Sonuç olarak, uygulamanızın geri kalanı (domain ve presentation-sunum katmanı) doğrudan type-safe model sınıfları/entities ile ilgilenebilir. Ayrıca kod tabanınız, bağımlı olduğunuz paketlerdeki değişiklikleri bozmaya karşı daha dirençli hale gelecektir._


[resource](https://codewithandrea.com/articles/flutter-repository-pattern/#keep-it-simple)