DEV Community

Cover image for Flutter Uygulama Mimarisi: Repository Pattern ­čĺź ­čîî ÔťĘ
G├╝lsen ­čŽő
G├╝lsen ­čŽő

Posted on

Flutter Uygulama Mimarisi: Repository Pattern ­čĺź ­čîî ÔťĘ

Tasar─▒m kal─▒plar─▒, yaz─▒l─▒m tasar─▒m─▒nda s─▒k kar┼č─▒la┼č─▒lan sorunlar─▒ ├ž├Âzmemize 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

Bu ba─člamda, veri katman─▒nda (data layer) depolar (repositories) 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─▒r─▒n.

ÔÇó Veri aktar─▒m nesnelerini (data transfer objects), domain katman─▒ taraf─▒ndan anla┼č─▒lan do─črulanm─▒┼č varl─▒klara (entities) d├Ân├╝┼čt├╝r├╝n.

ÔÇó (iste─če ba─čl─▒ olarak) data caching (veri ├Ânbelle─če alma) gibi i┼člemleri ger├žekle┼čtirin.

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─čunu d├╝┼č├╝nd├╝─č├╝m birka├ž kullan─▒m ├Ârne─či:

ÔÇó REST API'leri ile konu┼čmak
ÔÇó Yerel(local) veya uzak(remote) veritabanlar─▒yla konu┼čmak (├Ârn. Sembast, Hive, Firestore, vb.)
ÔÇó Cihaza ├Âzel API'lerle konu┼č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 API'sinden hava durumu verilerini ├žeken basit bir Flutter uygulamas─▒n─▒ inceleyece─čiz.

API belgelerini 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});
}

Enter fullscreen mode Exit fullscreen mode

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 veya 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
  }
}

Enter fullscreen mode Exit fullscreen mode

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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 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 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 kullanan bir ├Ârnek :

import 'package:get_it/get_it.dart';

GetIt.instance.registerLazySingleton<WeatherRepository>(
  () => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);
Enter fullscreen mode Exit fullscreen mode

─░┼čte 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());
});

Enter fullscreen mode Exit fullscreen mode

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(),
))
Enter fullscreen mode Exit fullscreen mode

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}) { ... }
}

Enter fullscreen mode Exit fullscreen mode

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 DioWeatherRepositoryyerine 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(...));
  }
}

Enter fullscreen mode Exit fullscreen mode

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 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(...)));
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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(...));
  });
}
Enter fullscreen mode Exit fullscreen mode

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) 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

Discussion (0)