Flutter App Architecture 1 — Repositories
Bu yazıda Flutter ile bir uygulama oluştururken mimariyle ilgili dikkat etmemiz gereken unsurları ele alan notlarımı paylaşıyor olacağım. Yazıyı oluştururken yüksek oranda buradaki flutter architecture serisinden yararlandım. Hazırladığım içerik anlatıcı bir içerikten ziyade, daha çok pratikte kullanma esnasında başucu olabilecek notlar ve pratiklerden oluşuyor.
Neden Mimari Gerekli?
- Eksikliği organizasyon eksikliğine ve aynı kodun tekrar tekrar kullanılması, değişiklik durumunda tüm kodun gözden geçirilmesi gibi sorunlara yol açar.
- Fazlası over-engineering ve en basit değişikliklerin çok uzun sürmesi gibi sorunlara yol açar.
Layers
Ok işaretleri katmanların birbirlerine olan bağımlılıklarını gösteriyor. Örneğin Repositories model kısmında bağlı ve sadece orayla ilgilenir. Services ise kendine data gerektiğinde bunu bir model olarak alır.
Architecture Örneği
Bir e-ticaret sitemizin sepet işlemlerini yönetiyor olalım. Bunun için sepete ürün ekleme, eklenen ürünün sayısını arttırıp azaltma gibi işlemlere ihtiyacımız var. Üstelik bu işlemler farklı widgetlardan ve farklı sayfalardan da yönetilebiliyor olacak. (Örn sepet düzenleme sayfası)
Sepet servisimiz eğer üye girişi yapılmışsa (auth service) back-end ile güncellenen uzak sunucuya, üye girişi yapılmamışsa cache kısmına eklenilen ürünleri kaydediyor olacak.
Bu senaryoyu oluştururken aşağıdaki gibi bir mantık kullanıyoruz:
Repositories (Data Layer):
- Data layer’da bulunan repositorylerin temel amaçları şu şekilde: Bir pakeyi projemizden soyutlamak, projemizi birden fazla data servisini (API ve Local Cache gibi) aynı anda kullanmaya elverişli hale getirmek. Dummy datalar ile test edilebilir kodlar yazmak.
- Örneğin harita servisi olarak Google Maps API kullanmaya karar verdiniz. Ancak daha sonra maliyetleri fazla geldiği için OpenStreetMap gibi alternatif çözümlere yöneldiniz. Böyle bir durumda haritanın sağladığı metotları önceden bir repository haline getirip soyutlama yaptığınız takdirde kodunuzun diğer taraflarında onlarca değişiklik yapmadan harita servisinizi değiştirebilirsiniz.
- Permission gibi birden fazla cihaza farklı kodlar gerektiren durumlarda da repository kullanılır.
Nasıl Yapılır?
Örneğimizde farklı network paketleri (veya fake bir paket) kullanarak hava durumu uygulaması için bir repository oluşturma örneklerini göreceğiz.
İlk olarak weather model (veya entity) oluşturalım. Bu class’ın .fromJson metodunu Rest API requestlerimiz için kullanacağız. Bu aslında domain layer’a ait. Bu modeli repository’den .fromJson metodunu kullanarak doldurmaya çalışıyoruz.
class Weather {
factory Weather.fromJson(Map<String, dynamic> json) {
// TODO: parse JSON and return validated Weather object
}
}
- Şimdi bir abstract class oluşturalım:
(Abstract class oluşturmak her senaryoda gerekli olmayabilir)
abstract class WeatherRepository {
Future<Weather> getWeather({required String city});
}
- Bu soyutlaştırmayı yaptık. Artık dilersek Http veya Dio kullanabiliriz. Daha sonra değiştirmek istediğimizde Dependency Injection yaptığımız yerden değiştirmemiz yeterli olacak.
- Http paketi için bir repository yazıyoruz. Burda interface’de yer almayan şeyler (client referansı gibi) de istiyoruz. Bunları daha sonra initialize ederken göndereceğiz.
class HttpWeatherRepository implements WeatherRepository {
HttpWeatherRepository({required this.api, required this.client});
final OpenWeatherMapAPI api;
final http.Client client;
Future<Weather> getWeather({required String city}) {
// TODO: send request, parse response, return Weather object or throw error
}
}
- Son olarak servislerde kullanacağımız class’ı initialize ediyoruz. Bunu yaparken genelde dependency injection kullanırız. Flutterda dependency injection yapmanın en kolay yolu Get.it paketi kullanmak.
import 'package:get_it/get_it.dart';
GetIt.instance.registerLazySingleton<WeatherRepository>(
() => HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client(),
);
Veya dilersek bunu Riverpod kullanarak da oluşturabiliriz:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return HttpWeatherRepository(api: OpenWeatherMapAPI(), client: http.Client());
});
Artık Http paketi yerine Dio’ya geçmek istediğimizde sadece burayı değiştirerek tüm servislerde aynı olmasını sağlayacağız.
Bonus: Fake test kullanarak test yazmak
class FakeWeatherRepository implements WeatherRepository {
Future<Weather> getWeather({required String city}) {
return Future.value(Weather(...));
}
}
NOTLAR:
- Flutterda abstract olmayan bir class’tan da implement yapabildiğin için sadece test yazabilmek için abstraction yapmana gerek yok.
- Repository’ler servislerin (application layer) aksine herhangi bir logic kullanmadan sadece genel geçer işlemleri (veri çekme, permission isteme) gibi düşünebilirsin. Bir bussines logic varsa bu servis katmanında yapılmalı.
SONUÇ:
Domain ve presentation katmanı type-safe (int mi string mi ne olduğu belli olan) modellere erişmeli, bunu sağlayacak olan ise repository katmanı.