Daha önce remember ve mutableStateOf‘u ele almıştık. Basit state için harika çalışıyorlar. Ama büyük bir sorunları var: uygulamanız bir veritabanından veya API’den veri çekmesi gerektiğinde, bu mantık nereye gidiyor?

Composable’ın içine değil. Composable’lar UI içindir, iş mantığı için değil.

ViewModel’in var olma sebebi bu.

ViewModel Nedir?

ViewModel, ekranınızın verisini ve mantığını tutan bir sınıf. Composable’ınızı yok edip yeniden oluşturan şeylerden (ekran döndürme veya sistem teması değişimi gibi) etkilenmez.

Şöyle düşünün:

ComposableViewModel
AmacıUI’ı göstermekVeri ve mantığı tutmak
Rotasyondan etkilenir miEvet (yeniden çizilir)Hayır
Süreç ölümünden etkilenir miEvetEvet (SavedStateHandle kullanın)
UI’dan haberdar mıEvetHayır
Yaşam döngüsüEkranla oluşturulur/yok edilirEkran var olduğu sürece yaşar

Kural şu: UI kodu Composable’larda yaşar. Geri kalan her şey ViewModel’de.

Neden Sadece remember Kullanmıyoruz?

// Bu çalışır... ta ki çalışmayana kadar
@Composable
fun UserListScreen() {
    var users by remember { mutableStateOf<List<User>>(emptyList()) }
    var isLoading by remember { mutableStateOf(true) }

    LaunchedEffect(Unit) {
        isLoading = true
        users = api.getUsers()  // Bir Composable içinde API çağrısı mı?
        isLoading = false
    }

    // Kullanıcıları göster...
}

Bu yaklaşımın sorunları:

  1. API çağrısı her rotasyonda yeniden başlar: remember, konfigürasyon değişikliklerinden kurtulamaz
  2. Mantık UI ile karışmış: test etmesi zor, bakımı zor
  3. Ayrım yok: aynı fonksiyon hem veri yüklemeyi hem gösterimi yönetiyor

ViewModel üçünü de çözer.

İlk ViewModel’iniz

Adım 1: ViewModel’i Oluşturun

class UserListViewModel : ViewModel() {

    // StateFlow veriyi tutar, Compose bunu gözlemler
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()

    private val _isLoading = MutableStateFlow(true)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    init {
        // ViewModel oluşturulduğunda veriyi yükle (her rotasyonda değil, bir kez)
        loadUsers()
    }

    private fun loadUsers() {
        viewModelScope.launch {
            _isLoading.value = true
            _users.value = listOf(
                User("1", "Alex", "alex@example.com"),
                User("2", "Sam", "sam@example.com"),
                User("3", "Jordan", "jordan@example.com"),
            )
            _isLoading.value = false
        }
    }

    fun deleteUser(userId: String) {
        _users.value = _users.value.filter { it.id != userId }
    }
}

data class User(val id: String, val name: String, val email: String)

Adım 2: ViewModel’i Compose’a Bağlayın

@Composable
fun UserListScreen(viewModel: UserListViewModel = viewModel()) {
    // collectAsStateWithLifecycle, StateFlow'u gözlemlemenin önerilen yolu
    // Uygulama arka plandayken toplamayı (collect) durdurur (kaynak tasarrufu)
    // Gerekli: implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
    val users by viewModel.users.collectAsStateWithLifecycle()
    val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()

    if (isLoading) {
        // Yükleniyor göstergesini göster
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CircularProgressIndicator()
        }
    } else {
        // Kullanıcı listesini göster
        LazyColumn {
            items(users, key = { it.id }) { user ->
                UserRow(
                    user = user,
                    onDelete = { viewModel.deleteUser(user.id) }
                )
            }
        }
    }
}

Bu kadar. ViewModel veriyi bir kez yükler. Rotasyon, yüklemeyi yeniden başlatmaz. UI sadece gözlemler ve gösterir.

StateFlow ve mutableStateOf Karşılaştırması

İkisi de state tutar. Hangisini ne zaman kullanmalısınız?

mutableStateOfStateFlow
NeredeComposable’ların içindeViewModel’lerin içinde
Compose’da gözlemlemeDoğrudan (by state)collectAsState()
Thread güvenli miHayır (sadece ana thread)Evet (herhangi bir thread)
Coroutine dostu muHayırEvet
Kullanım alanıSadece UI state’i (toggle, animasyon)ViewModel’den gelen veri

Basit kural: Composable’lar içinde UI state’i için remember ile mutableStateOf kullanın. ViewModel’ler içinde veri state’i için StateFlow kullanın.

Tek State Nesnesiyle ViewModel (Önerilen)

Birden fazla StateFlow yerine, her şeyi tek bir state sınıfında birleştirin:

// Tek bir data class, ekranın ihtiyacı olan her şeyi tutar
data class UserListState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = true,
    val error: String? = null,
    val searchQuery: String = ""
)

class UserListViewModel : ViewModel() {

    private val _state = MutableStateFlow(UserListState())
    val state: StateFlow<UserListState> = _state.asStateFlow()

    init {
        loadUsers()
    }

    private fun loadUsers() {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true, error = null) }

            try {
                val users = listOf(
                    User("1", "Alex", "alex@example.com"),
                    User("2", "Sam", "sam@example.com"),
                    User("3", "Jordan", "jordan@example.com"),
                    User("4", "Taylor", "taylor@example.com"),
                    User("5", "Morgan", "morgan@example.com"),
                )
                _state.update { it.copy(users = users, isLoading = false) }
            } catch (e: Exception) {
                _state.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }

    fun onSearchQueryChange(query: String) {
        _state.update { it.copy(searchQuery = query) }
    }

    fun deleteUser(userId: String) {
        _state.update { currentState ->
            currentState.copy(
                users = currentState.users.filter { it.id != userId }
            )
        }
    }

    fun retry() {
        loadUsers()
    }
}

Composable:

@Composable
fun UserListScreen(viewModel: UserListViewModel = viewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    Column(modifier = Modifier.fillMaxSize()) {
        // Arama çubuğu
        OutlinedTextField(
            value = state.searchQuery,
            onValueChange = { viewModel.onSearchQueryChange(it) },
            label = { Text("Search") },
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        )

        when {
            state.isLoading -> {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                }
            }
            state.error != null -> {
                Column(
                    modifier = Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text("Error: ${state.error}")
                    Button(onClick = { viewModel.retry() }) {
                        Text("Retry")
                    }
                }
            }
            else -> {
                // Kullanıcıları arama sorgusuna göre filtrele
                val filteredUsers = state.users.filter {
                    it.name.contains(state.searchQuery, ignoreCase = true)
                }

                LazyColumn {
                    items(filteredUsers, key = { it.id }) { user ->
                        UserRow(
                            user = user,
                            onDelete = { viewModel.deleteUser(user.id) }
                        )
                    }
                }
            }
        }
    }
}

Tek bir state nesnesi, ekranınızı öngörülebilir kılar. State sınıfına bakarak her zaman hangi verinin mevcut olduğunu bilirsiniz.

Argümanlarla bir ekrana geçiş yaptığınızda, ViewModel bunları SavedStateHandle ile okuyabilir:

@Serializable
data class UserProfile(val userId: String)

class UserProfileViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    // Navigasyon argümanını oku
    private val route = savedStateHandle.toRoute<UserProfile>()
    private val userId = route.userId

    private val _state = MutableStateFlow(ProfileState())
    val state: StateFlow<ProfileState> = _state.asStateFlow()

    init {
        loadProfile(userId)
    }

    private fun loadProfile(id: String) {
        viewModelScope.launch {
            // Gerçek bir uygulamada, ID'ye göre veritabanı veya API'den çek
            _state.update {
                it.copy(
                    name = "User $id",
                    email = "$id@example.com",
                    isLoading = false
                )
            }
        }
    }
}

data class ProfileState(
    val name: String = "",
    val email: String = "",
    val isLoading: Boolean = true
)

NavHost içinde:

composable<UserProfile> {
    // ViewModel, kaydedilmiş state handle ile otomatik oluşturulur
    val viewModel: UserProfileViewModel = viewModel()
    val state by viewModel.state.collectAsStateWithLifecycle()

    ProfileScreen(state = state)
}

Navigasyon argümanı geçer → SavedStateHandle saklar → ViewModel okur. Temiz ve tip güvenli.

viewModelScope ve Coroutine’ler

ViewModel, viewModelScope adında yerleşik bir coroutine scope’una sahip. ViewModel yok edildiğinde otomatik olarak iptal edilir.

class MyViewModel : ViewModel() {

    fun loadData() {
        // Bu coroutine, ViewModel yok edildiğinde otomatik olarak iptal edilir
        viewModelScope.launch {
            val data = repository.fetchData()  // Suspend fonksiyon
            _state.update { it.copy(data = data) }
        }
    }

    fun loadMultipleThings() {
        viewModelScope.launch {
            // İki işlemi paralel çalıştır
            val users = async { repository.getUsers() }
            val posts = async { repository.getPosts() }

            _state.update {
                it.copy(
                    users = users.await(),
                    posts = posts.await()
                )
            }
        }
    }
}

Bir ViewModel içinde asla GlobalScope kullanmayın veya kendi CoroutineScope‘unuzu oluşturmayın. Her zaman viewModelScope kullanın.

State Değişiklikleri: ViewModel mi, Composable mi?

Net bir rehber:

ViewModel’de Tutun (StateFlow)

  • Veritabanından veya API’den gelen veri
  • Kullanıcı listesi, arama sonuçları, form verisi
  • Yükleniyor durumu, hata mesajları
  • Rotasyondan kurtulması gereken her şey

Composable’da Tutun (remember)

  • Açılır menü açık mı?
  • Mevcut kaydırma pozisyonu
  • Hangi metin alanı odakta?
  • Animasyon ilerlemesi
  • Rotasyonda sıfırlanması sorun olmayan her şey

Örnek: İkisi Bir Arada

@Composable
fun UserListScreen(viewModel: UserListViewModel = viewModel()) {
    // ViewModel state'i: rotasyondan kurtulur
    val state by viewModel.state.collectAsStateWithLifecycle()

    // UI state'i: rotasyonda sıfırlanması sorun değil
    var showDeleteDialog by remember { mutableStateOf(false) }
    var selectedUser by remember { mutableStateOf<User?>(null) }

    // ... her iki state türünü de kullan
}

Türkiye’deki Android İş İlanlarında ViewModel Beklentisi

Türkiye’deki Android/Kotlin iş ilanlarına (LinkedIn, kariyer.net, Kotlin Türkiye topluluğunun paylaşımları) bakıldığında, “MVVM” ve “ViewModel” neredeyse her orta ve kıdemli seviye ilanda geçen bir gereksinim, ama ilanların çoğu bunun ötesine geçip StateFlow ile birlikte kullanılan tek state nesnesi (bu yazıdaki UserListState deseni) veya MVI mimarisini açıkça arıyor. Bunun nedeni basit: mülakatlarda adaylara genellikle “ViewModel’de neden birden fazla StateFlow yerine tek bir state class kullanırsınız” gibi bir soru soruluyor ve sadece mutableStateOf ile remember‘ı bilen ama ViewModel’i hiç kullanmamış adaylar bu noktada zorlanıyor. Junior seviye bir Android geliştirici pozisyonuna başvuracaksanız, viewModelScope‘un yaşam döngüsüyle nasıl otomatik iptal olduğunu ve collectAsStateWithLifecycle‘ın neden düz collectAsState‘ten daha güvenli olduğunu somut örneklerle açıklayabilmek, mülakatlarda öne çıkan adaylarla çıkamayanlar arasındaki en belirgin farklardan biri.

Sık Yapılan Hatalar

Hata 1: ViewModel’i Manuel Oluşturmak

// KÖTÜ: her yeniden kompozisyonda yeni bir örnek
val viewModel = UserListViewModel()

// İYİ: Compose yaşam döngüsünü yönetir
val viewModel: UserListViewModel = viewModel()

Hata 2: StateFlow’u collectAsState Olmadan Gözlemlemek

// KÖTÜ: değer, yeniden kompozisyonu tetiklemiyor
val users = viewModel.users.value

// İYİ: değer değiştiğinde yeniden kompozisyonu tetikler
val users by viewModel.users.collectAsState()

Hata 3: ViewModel’i Alt Composable’lara Geçirmek

// KÖTÜ: alt bileşen ViewModel'i biliyor
@Composable
fun UserRow(viewModel: UserListViewModel, user: User) { ... }

// İYİ: alt bileşen sadece ihtiyacı olanı alıyor
@Composable
fun UserRow(user: User, onDelete: () -> Unit) { ... }

Hata 4: ViewModel’de UI İşi Yapmak

// KÖTÜ: ViewModel, UI'dan haberdar olmamalı
class MyViewModel : ViewModel() {
    fun showToast(context: Context) { // Context geçirmeyin!
        Toast.makeText(context, "Done", Toast.LENGTH_SHORT).show()
    }
}

// İYİ: ViewModel bir olay gönderir, UI gösterimi yönetir
class MyViewModel : ViewModel() {
    private val _showMessage = MutableStateFlow<String?>(null)
    val showMessage: StateFlow<String?> = _showMessage

    fun doSomething() {
        _showMessage.value = "Done"
    }
}

Hızlı Referans

İşlemKod
ViewModel oluşturval viewModel: MyViewModel = viewModel()
State’i dışa verval state: StateFlow<MyState> = _state.asStateFlow()
State’i güncelle_state.update { it.copy(loading = true) }
Compose’da gözlemleval state by viewModel.state.collectAsStateWithLifecycle()
Coroutine başlatviewModelScope.launch { ... }
Navigasyon argümanı okusavedStateHandle.toRoute<MyRoute>()

İlgili Yazılar