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:
| Composable | ViewModel | |
|---|---|---|
| Amacı | UI’ı göstermek | Veri ve mantığı tutmak |
| Rotasyondan etkilenir mi | Evet (yeniden çizilir) | Hayır |
| Süreç ölümünden etkilenir mi | Evet | Evet (SavedStateHandle kullanın) |
| UI’dan haberdar mı | Evet | Hayır |
| Yaşam döngüsü | Ekranla oluşturulur/yok edilir | Ekran 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ı:
- API çağrısı her rotasyonda yeniden başlar:
remember, konfigürasyon değişikliklerinden kurtulamaz - Mantık UI ile karışmış: test etmesi zor, bakımı zor
- 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?
mutableStateOf | StateFlow | |
|---|---|---|
| Nerede | Composable’ların içinde | ViewModel’lerin içinde |
| Compose’da gözlemleme | Doğrudan (by state) | collectAsState() |
| Thread güvenli mi | Hayır (sadece ana thread) | Evet (herhangi bir thread) |
| Coroutine dostu mu | Hayır | Evet |
| 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.
Navigasyon Argümanlarıyla ViewModel
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
| İşlem | Kod |
|---|---|
| ViewModel oluştur | val viewModel: MyViewModel = viewModel() |
| State’i dışa ver | val state: StateFlow<MyState> = _state.asStateFlow() |
| State’i güncelle | _state.update { it.copy(loading = true) } |
| Compose’da gözlemle | val state by viewModel.state.collectAsStateWithLifecycle() |
| Coroutine başlat | viewModelScope.launch { ... } |
| Navigasyon argümanı oku | savedStateHandle.toRoute<MyRoute>() |