Android geliştirme son birkaç yılda önemli ölçüde değişti. Modern bir Android uygulaması artık yalnızca bir Activity, birkaç XML layout ve birkaç API çağrısından ibaret değil.
Günümüzdeki uygulamaların asenkron veriyi, yapılandırma değişikliklerini, çevrimdışı davranışı, testleri, bağımlılık yönetimini, arka plan işlerini, ölçeklenebilirliği ve giderek karmaşıklaşan kullanıcı arayüzlerini yönetmesi gerekiyor.
Production uygulamalarında hedef yalnızca uygulamanın çalışmasını sağlamak olmamalı. Ürün büyüdükçe sürdürülebilir, test edilebilir, ölçeklenebilir ve anlaşılır kalan bir yazılım inşa etmek gerekir.
Tipik bir modern Android mimarisi şöyle gösterilebilir:
User
│
▼
Jetpack Compose UI
│
User Events
│
▼
ViewModel
│
StateFlow / Flow
│
▼
Use Case
│
▼
Repository
/ \
/ \
Remote API Local Data
Retrofit/Ktor Room/DataStore
\ /
\ /
Data Layer
Şimdi her parçanın sorumluluğuna ve bu ayrımın neden önemli olduğuna bakalım.
1. Temel Taş Olarak Kotlin
Modern native Android geliştirmede Kotlin genellikle varsayılan dil olmalıdır. Null safety, coroutine’ler, extension function’lar, sealed type’lar, data class’lar ve mevcut Java koduyla güçlü birlikte çalışabilirlik gibi özellikler sunar.
Örneğin uygulama durumu bir sealed interface kullanılarak temiz biçimde modellenebilir:
sealed interface UiState {
data object Loading : UiState
data class Success(
val data: List<Device>
) : UiState
data class Error(
val message: String
) : UiState
}
Bu yaklaşım, uygulama durumunu birbiriyle ilgisiz Boolean değişkenleriyle ifade etmekten çok daha güvenlidir:
isLoading
hasError
hasData
Bu değişkenler yanlışlıkla imkânsız durum kombinasyonları oluşturabilir. Sealed hierarchy, geçerli durumları açıkça tanımlar.
2. Declarative UI için Jetpack Compose
Jetpack Compose, Android arayüzlerinin tasarlanma biçimini değiştirir. Geleneksel UI geliştirme büyük ölçüde imperative bir yaklaşıma dayanır:
Find View
↓
Change View
↓
Update another View
↓
Hide something
↓
Show something else
Compose ise declarative yaklaşımı teşvik eder. Arayüze nasıl değişeceğini tek tek söylemek yerine, mevcut durum için nasıl görünmesi gerektiğini tanımlarız.
@Composable
fun DeviceScreen(
state: UiState
) {
when (state) {
UiState.Loading -> LoadingScreen()
is UiState.Success ->
DeviceList(state.data)
is UiState.Error ->
ErrorScreen(state.message)
}
}
Temel fikir şudur:
State
↓
UI
Durum değiştiğinde Compose, recomposition aracılığıyla arayüzün gerekli bölümlerini günceller. Bu, modern Android geliştirmenin en önemli ilkelerinden birine götürür: UI, uygulama durumunun bir temsilidir.
3. Business Logic Composable Fonksiyonların Dışında Kalmalı
Composable fonksiyonlar öncelikle durumu göstermekten ve kullanıcı etkileşimlerini bildirmekten sorumlu olmalıdır. Networking, veritabanı işlemleri ve iş kuralları için bir konteyner hâline gelmemelidir.
Şu tür tasarımlardan kaçının:
Composable
├── HTTP request
├── database query
├── business rules
├── state management
└── UI rendering
Daha sağlıklı yapı şöyledir:
Composable
│
│ event
▼
ViewModel
│
▼
Use Case / Repository
Örneğin:
@Composable
fun DeviceScreen(
state: DeviceUiState,
onRefresh: () -> Unit
) {
// Render state
}
Bu yapı Composable fonksiyonların preview edilmesini, yeniden kullanılmasını ve test edilmesini kolaylaştırır.
4. UI State Holder Olarak ViewModel
ViewModel, UI ile uygulamanın geri kalanı arasında köprü görevi görür. Basitleştirilmiş bir örnek:
class DeviceViewModel(
private val repository: DeviceRepository
) : ViewModel() {
private val _uiState =
MutableStateFlow<DeviceUiState>(
DeviceUiState.Loading
)
val uiState: StateFlow<DeviceUiState> =
_uiState.asStateFlow()
fun loadDevices() {
viewModelScope.launch {
// Load data
}
}
}
Burada önemli bir ayrıntı vardır: `_uiState` içeride mutable durumdadır; ancak `uiState` immutable state olarak dışarı açılır. UI durumu gözlemleyebilir, fakat onu rastgele değiştiremez.
Bu, verinin öngörülebilir bir yönde akmasını sağlar:
State
ViewModel ────────► UI
ViewModel ◄──────── UI
Events
Bu desen genellikle Unidirectional Data Flow olarak adlandırılır.
5. StateFlow ve Flow
Modern uygulamalar doğal olarak asenkron çalışır. Bir cihazın durumu değişebilir, veritabanı yeni kayıtlar yayınlayabilir, API isteği tamamlanabilir, bağlantı kopabilir veya kullanıcı ayarları değiştirebilir.
Bu değişiklikleri stream olarak temsil etmek mimariyi anlamayı kolaylaştırır. Kotlin Flow asenkron bir stream abstraction sunar. StateFlow ise her zaman güncel bir değer içerdiği için UI state için özellikle kullanışlıdır.
Tipik zincir şöyledir:
Repository
│
▼
Flow
│
▼
ViewModel
│
▼
StateFlow
│
▼
Compose
│
▼
Recomposition
Compose içinde durum lifecycle-aware şekilde toplanabilir:
val state by viewModel.uiState
.collectAsStateWithLifecycle()
Böylece arayüz durum değişikliklerine doğal biçimde tepki verir.
6. Coroutine ve Structured Concurrency
Android uygulamaları ağ istekleri, veritabanı işlemleri, dosya erişimi, cihaz iletişimi, arka plan senkronizasyonu ve CPU yoğun işlemler gibi pek çok asenkron iş gerçekleştirir.
Bu işlemler ana thread’i bloklamamalıdır. Kotlin Coroutine’leri asenkron çalışmanın temelini sağlar.
viewModelScope.launch {
val devices = repository.getDevices()
}
Yaygın bir yanlış anlama şudur:
suspend = background thread
Bu doğru değildir. Suspend fonksiyonu thread’i bloklamadan çalışmayı askıya alabilir; ancak hangi execution context içinde çalıştığı yine önemlidir.
Yaygın dispatcher’lar şunlardır:
Dispatchers.Main
UI work
Dispatchers.IO
Blocking I/O
Dispatchers.Default
CPU-intensive work
Structured concurrency de aynı derecede önemlidir. `viewModelScope` kullanmak coroutine yaşam süresini ViewModel’a bağlar ve kontrolsüz arka plan işlerinin süresiz biçimde devam etmesini önler.
7. Repository Pattern
UI verinin REST servisinden mi, Bluetooth’tan mı, veritabanından mı, cache’ten mi, dosyadan mı veya başka bir cihazdan mı geldiğini bilmemelidir. Repository’nin sorumluluklarından biri budur.
Örneğin:
interface DeviceRepository {
fun observeDevices(): Flow<List<Device>>
suspend fun refreshDevices()
}
ViewModel yalnızca abstraction’ı bilir. Gerçek implementasyon bugün bir REST API ile, yarın ise yerel bir veritabanıyla iletişim kurabilir.
ViewModel
│
▼
DeviceRepository
/ \
▼ ▼
Remote API Room
Bu yaklaşım katmanlar arasındaki coupling’i azaltır.
8. Dependency Injection
Şu durumu düşünün:
class DeviceViewModel : ViewModel() {
private val repository =
DeviceRepositoryImpl()
}
Bu durumda ViewModel kendi dependency’sini oluşturmaktan sorumludur. Bu gereksiz coupling oluşturur ve testleri zorlaştırır.
Bunun yerine:
class DeviceViewModel(
private val repository: DeviceRepository
) : ViewModel()
Dependency dışarıdan sağlanır. Hilt gibi kütüphaneler daha büyük Android uygulamalarında dependency graph’ını yönetebilir. Dependency Injection loose coupling, test edilebilirlik, değiştirilebilir implementasyonlar ve merkezi dependency yönetimi sağlar.
9. Overengineering Yapmadan Clean Architecture
Clean Architecture sorumlulukları açık katmanlara ayırmayı önerir:
Presentation
Contains things such as:
Compose
ViewModel
UiState
Domain
Contains application/business rules:
Use Cases
Domain Models
Repository Interfaces
Data
Contains infrastructure:
Repository Implementations
Retrofit/Ktor
Room
DataStore
DTOs
Data Sources
Örneğin:
DeviceScreen
↓
DeviceViewModel
↓
ObserveDevicesUseCase
↓
DeviceRepository
↓
DeviceRepositoryImpl
↙ ↘
Room REST API
Ancak Clean Architecture bir inanç sistemine dönüşmemelidir. Küçük bir uygulamanın tek bir değeri okumak için Screen, ViewModel, UseCase, Interactor, Repository, DataSource ve bir başka abstraction katmanına ihtiyacı olmayabilir.
Mimari karmaşıklığı yönetmek için vardır; yalnızca sofistike görünmek için yeni bir karmaşıklık üretmemelidir.
10. Modularization
Uygulama büyüdükçe işlevleri Gradle modüllerine ayırmak sürdürülebilirliği artırabilir. Olası bir yapı şöyle görünebilir:
app/
core/
├── common/
├── designsystem/
├── network/
├── database/
└── model/
feature/
├── home/
├── devices/
├── settings/
└── profile/
Feature modülleri net sorumluluklara sahip olabilir. Bu yaklaşım ownership, build organizasyonu, dependency sınırları, reuse ve test edilebilirliği iyileştirebilir. Ancak modülerleştirme de gerçek bir problemi çözmelidir. Küçük bir uygulama için 40 modül oluşturmak otomatik olarak iyi mimari anlamına gelmez.
11. Networking
Tipik bir network stack şu şekilde olabilir:
Compose
↓
ViewModel
↓
Repository
↓
Retrofit / Ktor
↓
HTTP
↓
Backend
Network hataları beklenmedik durumlar gibi değil, normal uygulama durumları gibi ele alınmalıdır. Uygulama bağlantı yokluğu, timeout, authentication failure, server error, hatalı response, rate limiting ve geçici failure durumlarını hesaba katmalıdır.
Production uygulaması her ilgili failure mode için bilinçli bir stratejiye ihtiyaç duyar.
12. Local Persistence
Her veri aynı storage sistemine ait değildir.
Room
Useful for structured relational data and offline caching.
DataStore
Useful for smaller preferences such as:
Theme
Units
User settings
Feature preferences
Files
Useful for larger binary resources such as media.
Repository katmanı yerel ve uzak kaynakları koordine edebilir:
Repository
/ \
/ \
Local Remote
Room API
Bu yapı offline-first mimarilerin de temelini oluşturur.
13. Hata Yönetimi Mimarinin Bir Parçası Olmalı
Production uygulaması happy path’e güvenmemelidir. Bağlantılı bir cihaz uygulamasında şu soruları düşünün:
The device disconnects?
Bluetooth is disabled?
Permission is denied?
The network disappears?
The application goes into the background?
The process is recreated?
The server returns invalid data?
The user presses Connect ten times?
Bunlar olağandışı durumlar değildir; normal uygulama durumlarıdır.
sealed interface ConnectionState {
data object Disconnected : ConnectionState
data object Connecting : ConnectionState
data object Connected : ConnectionState
data class Error(
val cause: Throwable
) : ConnectionState
}
Durumlar hakkında açıkça düşünmek edge case’leri yönetmeyi kolaylaştırır.
14. Testler Birden Fazla Seviyede Var Olmalı
Sürdürülebilir bir proje yalnızca manuel testlere güvenmemelidir. Pratik bir test stratejisi birden fazla katmandan oluşur.
UI / E2E Tests
▲
/ \
/ \
Integration Tests
▲
/ \
/ \
Unit Tests
Unit test’ler hızlıdır ve önemli iş mantığını kapsamalıdır. Integration test’ler bileşenlerin birlikte doğru çalıştığını doğrular. UI test’leri önemli kullanıcı yolculuklarını kontrol eder.
Önemli olan test sayısını en yüksek seviyeye çıkarmak değil, önemli davranışları uygun maliyetli seviyede korumaktır.
15. Test Edilebilirlik için Tasarlayın
Testler mimariyi etkilemelidir. ViewModel her şeyi kendisi oluşturuyorsa test zorlaşır:
class DeviceViewModel : ViewModel() {
private val api = RealApi()
private val database = RealDatabase()
}
Dependency inversion ile sahte bir implementasyon sağlanabilir:
class DeviceViewModel(
private val repository: DeviceRepository
)
class FakeDeviceRepository :
DeviceRepository {
// deterministic test behavior
}
İyi mimari ile iyi test pratikleri birbirini güçlendirir.
16. Performance: Optimize Etmeden Önce Ölçün
Performans optimizasyonu kanıtla başlamalıdır. “Bu kod yavaş olabilir” diye tahmin etmek yerine şu döngüyü izleyin:
Measure
↓
Find bottleneck
↓
Form hypothesis
↓
Optimize
↓
Measure again
Android geliştiricileri CPU kullanımı, memory allocation, memory leak’ler, excessive recomposition, yavaş rendering, network davranışı, main-thread blocking ve startup performance gibi alanları inceleyebilmelidir. İlke basittir: Önce profile edin, sonra optimize edin.
17. Native C++ Ne Zaman Gerçekten Anlamlıdır?
Android NDK, native C/C++ kodunun Android uygulamasına entegre edilmesini sağlar. Kavramsal olarak:
Kotlin
│
▼
JNI
│
▼
C++
Ancak JNI ek karmaşıklık ve sınır geçişi maliyeti getirir. Native kodu etkileyici göründüğü için değil, problem gerçekten gerektirdiği için kullanın.
18. CI/CD Projenin Bir Parçasıdır
GitHub Actions gibi araçlar build, lint, test, güvenlik kontrolleri ve deployment akışlarını otomatikleştirebilir. Amaç basittir: Hatalı değişikliklerin merge edilmesini zorlaştırmak, doğru değişikliklerin release edilmesini kolaylaştırmak.
19. Pratik Bir Modern Android Stack
UI
└── Jetpack Compose
└── Material 3
Concurrency
├── Coroutines
└── Flow / StateFlow
Architecture
├── MVVM
├── Unidirectional Data Flow
├── Repository Pattern
└── Clean Architecture where justified
Dependency Injection
└── Hilt
Networking
├── Retrofit
└── Ktor
Persistence
├── Room
└── DataStore
Background Work
└── WorkManager
Testing
├── JUnit
├── Integration Tests
└── Compose UI Tests
Build / CI
├── Gradle
└── GitHub Actions
Monitoring
├── Crash Reporting
└── Analytics
Native
└── C++ / NDK / JNI when required
Ancak kullanılan teknolojiler mimarinin kendisi değildir. Bu ayrım önemlidir.
20. Büyük Resim
Hatırlanması gereken tek bir diyagram varsa, o da şudur:
USER
│
▼
┌─────────────────┐
│ Jetpack Compose │
└────────┬────────┘
│ Events
▼
┌─────────────────┐
│ ViewModel │
│ StateFlow │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Use Cases │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Repository │
└───────┬─────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
REST API Room Device
│ │ │
└────────────┼────────────┘
│
▼
Flow
│
▼
ViewModel
│
▼
StateFlow
│
▼
Compose
│
▼
Recomposition
Bu yapı öngörülebilir bir döngü oluşturur: Kullanıcı eylemi → ViewModel → iş/veri katmanı → yeni durum → UI güncellemesi. Bu akış net olduğunda proje anlaşılması, debug edilmesi, test edilmesi ve genişletilmesi çok daha kolay hâle gelir.
Sonuç
Modern bir Android projesini kullandığı kütüphane sayısı tanımlamaz. Compose, Hilt, Room, Coroutines ve Clean Architecture kullanmak tek başına iyi yazılım üretmez.
İyi Android mimarisi birkaç temel soruya net cevaplar vermekle ilgilidir: Durumun sahibi kim? İş mantığı nerede yaşar? Veri uygulama içinde nasıl hareket eder? Bağımlılıklar nasıl yönetilir? Bir şey başarısız olduğunda ne olur? Önemli davranışlar test edilebilir mi? Başka bir geliştirici tüm kod tabanını okumadan sistemi anlayabilir mi?
Modern Android geliştirme birkaç ilkeye dayanır:
- Durumu öngörülebilir tutun.
- Bağımlılıkları açıkça tanımlayın.
- Sorumlulukları ayırın.
- Hata ve lifecycle değişiklikleri için tasarlayın.
- Önemli davranışları test edin.
- Optimize etmeden önce ölçün.
- Karmaşıklığı yalnızca gerçek bir problemi çözdüğünde ekleyin.
Son ilke belki de en önemlisidir. En iyi mimari, en fazla katmana sahip mimari değildir. En iyi mimari, ürünün karmaşıklığını yönetirken kendisi yeni bir karmaşıklık kaynağına dönüşmeyen en basit mimaridir.