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.