ANDROID JETPACK CLEAN ARCHITECTURE
Bir Android projesi başlangıçta tek Activity ile başlar. Birkaç ekran, bir-iki fragment, küçük bir Room veritabanı eklenir. Altıncı ayda Activity bin satırı geçer; UI mantığı, ağ çağrısı, veritabanı işlemi aynı sınıfa sığar. Test yazmak imkansız hale gelir, basit bir kural değişikliği üç farklı dosyada tetikleme yapar. Android Jetpack, Google'ın bu kaosu önlemek için sunduğu kütüphane ailesidir. Clean Architecture ise bu kütüphaneleri katmanlara yerleştirmenin sağlam bir reçetesidir. İkisini birlikte uyguladığında ekibe katılan yeni geliştirici, ekranı değil sorumluluk haritasını okur — değişikliğin nereye yapılacağı net olur.
Clean Architecture Hangi Soruyu Cevaplıyor?
Clean Architecture, Robert C. Martin'in 2012'de derlediği "katmanlı, bağımlılığı içe doğru yöneten" bir tasarım yaklaşımıdır. Üç temel katman vardır:
- Domain (alan): İş kuralları, varlıklar, use case'ler. Android'e veya herhangi bir framework'e bağımlı değildir.
- Data (veri): Repository implementasyonları, ağ servisleri, veritabanı erişimi, cache stratejileri.
- Presentation (sunum): ViewModel, Composable veya XML view, UI state akışı, navigation.
Kural basit: Bağımlılık oku içe doğru gider. Presentation Domain'i bilir, Data Domain'i bilir; ama Domain ne Presentation'ı ne de Data'yı bilir. Yani Domain'i Android projesinden çıkarıp düz JVM testleriyle çalıştırabilirsin. Bu testlenebilirlik, mimarinin asıl getirisidir. Android resmi mimari rehberi de aynı içe doğru bağımlılık prensibini "data → domain ← ui" şemasıyla anlatır.
Android Jetpack Bileşenleri Hangi Katmana Yerleşir?
Jetpack; lifecycle, mimari, UI ve davranış olmak üzere dört temel kategoride kütüphane sunar. Clean Architecture katmanlarına şöyle dağılırlar:
- Presentation: ViewModel, LiveData, Compose, Navigation, Paging
- Data: Room, DataStore, WorkManager, Retrofit (Jetpack dışı ama bu katmana ait)
- Çapraz kesim (cross-cutting): Hilt (DI), Lifecycle, Kotlin Coroutines
Use case'ler ve domain modelleri saf Kotlin sınıflarıdır — Jetpack içermez. Bu sınırı korumak mimariyi sürdürülebilir kılar. Android tarafında ortaya çıkan değişiklikler — yeni UI kütüphanesi, ekran formatı, navigation modeli — domain'i etkilemez. Android Kotlin tarafında temel pratiğe geçmek isteyenler için Android Kotlin eğitim programı Jetpack bileşenlerini örnek proje üzerinden ilerletir.

Katman Yapısı: Modül mü Paket mi?
Pratikte iki yaklaşım vardır. Küçük projelerde tek modül içinde paket ayrımı yeterlidir:
com.example.app.domaincom.example.app.datacom.example.app.presentation
Orta ve büyük projelerde Gradle modülü ayrımı tercih edilir: :domain, :data, :app. Bu yaklaşım derleme süresini paralelleştirir, bağımlılığın yön ihlalini Gradle seviyesinde imkansız hale getirir. Domain modülü Android bağımlılığı içermez — saf Kotlin'dir. Data modülü Room ve Retrofit gibi Android/JVM kütüphaneleri kullanır. App modülü ise Compose, Hilt ve Navigation içerir.
Domain Katmanı: Use Case Pattern
Use case (Türkçesi kullanım senaryosu) iş kurallarını taşıyan, tek sorumlu sınıftır. Genellikle bir invoke() operatörü vardır:
class GetUserProfileUseCase(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: String): UserProfile {
return userRepository.getUser(userId)
}
}Use case'in iki temel faydası vardır. Birincisi, ViewModel'a tek bir operasyon enjekte edilir — repository'nin tüm metotları değil. İkincisi, business rule değişirse (örnek: "kullanıcı banlanmışsa profil göstermeden hata fırlat") yeni mantık use case'e eklenir, ViewModel'a değil. Böylece ViewModel'lar ince kalır.
Data Katmanı: Repository Pattern
Repository, veri kaynağını domain'den soyutlar. Arayüz domain katmanında tanımlanır, implementasyon data katmanında bulunur. Bu, bağımlılığı tersine çevirme (dependency inversion) prensibinin tipik uygulanışıdır.
// domain
interface UserRepository {
suspend fun getUser(id: String): UserProfile
}
// data
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override suspend fun getUser(id: String): UserProfile {
val cached = dao.findById(id)
if (cached != null) return cached.toDomain()
val remote = api.fetchUser(id)
dao.save(remote.toEntity())
return remote.toDomain()
}
}Cache-then-network, network-then-cache veya sadece veritabanı — strateji repository içinde gizlidir. UI tarafı bu detayı bilmez. Yarın Room yerine SQLDelight'a geçilirse ViewModel kodu değişmez. Mapping fonksiyonları (toDomain(), toEntity()) entity ile domain modelini birbirinden ayrı tutar; veritabanı kolonu eklendiğinde domain modeli kirlenmez.
Presentation: ViewModel ve UI State
Modern Android'de Compose ile birlikte tek bir StateFlow<UiState> akışı tercih edilir. loading, data ve error'ı ayrı LiveData'larda tutmak tutarsız ara durum riski yaratır; tek state nesnesi atomik geçiş sağlar.
data class ProfileUiState(
val loading: Boolean = false,
val profile: UserProfile? = null,
val error: String? = null
)
class ProfileViewModel(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
private val _state = MutableStateFlow(ProfileUiState())
val state: StateFlow<ProfileUiState> = _state.asStateFlow()
fun load(userId: String) {
viewModelScope.launch {
_state.update { it.copy(loading = true) }
runCatching { getUserProfile(userId) }
.onSuccess { p -> _state.value = ProfileUiState(profile = p) }
.onFailure { e -> _state.value = ProfileUiState(error = e.message) }
}
}
}ViewModel iş kuralı içermez; sadece UI state'i yönetir, use case'i çağırır, hataları sunum hatasına çevirir. Composable bu state'i collectAsStateWithLifecycle() ile dinler ve ekranı yeniden çizer. Lifecycle yönetimi tamamen Jetpack'in kontrolündedir; yapılandırma değişikliklerinde state korunur.
Hilt ile Bağımlılık Yönetimi
Katmanlar arasındaki bağımlılığı manuel kurmak küçük projede çalışır, büyüklerde çığ gibi büyür. Hilt, Dagger'ın Android için sadeleştirilmiş halidir ve standart bileşen kapsamlarını (Application, ViewModel, Activity, Fragment) hazır sunar.
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel()Interface domain'de, @Binds bağlaması data modülünde durur. ViewModel kendi katmanında use case'i alır, repository implementasyonunu hiç görmez. Test yazarken use case fake repository ile manuel kurulur; Hilt sadece prod build için gerekli olur.

Yaygın Tuzaklar
- Domain'e Android import etmek:
Context,Resources,android.net.Uridomain'de görünmemeli. Görünürse mimariyi delmişsindir; hata mesajınıR.string'den almak yerine error code üretip presentation'da Türkçeleştir. - Boş use case zorlaması: Tek satır repository çağrısı yapan use case ilk bakışta gereksiz görünür. Küçük CRUD'da gerçekten gereksizdir; orta-büyük projede ileride business rule eklenir, o gün ViewModel kalabalıklaşmaz.
- Üçlü LiveData ile UI state: Eski örneklerde
LiveData<Profile>,LiveData<Boolean>loading veLiveData<String>error ayrı tutulurdu. Üçü ayrı emit edildiğinde "yüklendi ama eski hata hâlâ orada" gibi tutarsız durumlar çıkar. - Repository'de UI string üretmek: Repository
Result<Domain>veya domain exception fırlatır; "kullanıcıya gösterilecek mesaj" üretmez. O mesajı presentation kurar. - ViewModel'a Context geçmek: ViewModel
Applicationdışında Context bağımlılığı taşırsa memory leak yaratabilir. String kaynakları Composable veya Activity tarafında çözülmeli.
Ne Zaman Bu Mimariyi Kurmamalı?
Hızlı prototip, tek ekranlı uygulama veya öğrenme amaçlı küçük örnek için Clean Architecture aşırı maliyettir. Boilerplate kodu ekibe değer üretmeden çoğalır. Mimari kararı projenin ölçeğine ve ekibin büyüklüğüne göre verilir.
Üç haftalık MVP için ViewModel + Repository ikilisi yeterli olur. Yıllarca yaşayacak, birden fazla geliştiricinin paralel çalışacağı kurumsal app için ise katman ayrımı uzun vadede zaman kazandırır. Mimari saf akademik tercih değil — ekibin değişiklik hızını ve test güvenini belirleyen pratik bir karardır.



