Dolap Hafızası örneğinde Jetpack Compose, ViewModel, StateFlow, Repository ve Room arasındaki veri akışının teknik gösterimi.
Jetpack Compose MVVM Room mimarisi, Kotlin ile geliştirilen Android uygulamalarında kullanıcı arayüzünü, iş mantığını ve veritabanı işlemlerini birbirinden ayırmak için kullanılabilecek güçlü bir yaklaşım. Ancak bu teknolojilerin isimlerini bilmek ile gerçek bir projede doğru şekilde kullanmak arasında önemli bir fark var.
Kotlin ve Jetpack Compose ile geliştirdiğim Dolap Hafızası uygulamasında bu farkı yakından deneyimledim.
Dolap Hafızası, evdeki eşyaların nerede saklandığını hatırlamayı kolaylaştıran bir Android uygulaması. Kullanıcılar oda ve saklama alanları oluşturabiliyor, eşyalarını kaydedebiliyor, fotoğraf ve not ekleyebiliyor, favoriler oluşturabiliyor ve son kullanma tarihi gibi bilgileri takip edebiliyor.
İlk bakışta basit bir eşya kayıt uygulaması gibi görünse de özellikler arttıkça veri yönetimi, ekran durumları, arka plan işlemleri ve kodun sürdürülebilirliği daha önemli hale geliyor.
Bu yazıda Dolap Hafızası’nın kullandığı Kotlin, Jetpack Compose, MVVM, Room, Flow ve StateFlow teknolojilerini temel alarak gerçek bir Android uygulamasının mimarisini adım adım inceleyeceğiz.
Önemli not: Bu yazıdaki kodlar, Dolap Hafızası’nın gerçek özelliklerinden ve kullandığım mimari yaklaşımdan hareketle hazırlanmış sadeleştirilmiş eğitim örnekleridir. Uygulamanın mevcut kaynak kodunun birebir kopyası değildir. Amacım, aynı yaklaşımı kendi Android projelerinde kullanmak isteyen geliştiricilere anlaşılır ve uygulanabilir bir örnek sunmak.
Dolap Hafızası’nı geliştirirken çözmek istediğim temel problem oldukça basitti:
“Bu eşyayı nereye koymuştum?”
Evde bulunan bir eşyanın hangi odada, hangi dolapta veya saklama alanında olduğunu hatırlamak bazen zor olabiliyor.
Bu problemi çözmek için kullanıcıların eşyalarını kategorize edebildiği, saklama yerlerini tanımlayabildiği ve daha sonra kayıtlarına ulaşabildiği bir yapı oluşturdum.
Uygulamadaki bazı temel özellikler şunlar:
Bu özellikleri tek bir Activity veya birkaç büyük Compose dosyası içerisinde yönetmeye çalışmak, proje büyüdükçe bakım maliyetini artırabilirdi.
Örneğin bir ekranda eşya listesini göstermek, başka bir ekranda yeni eşya kaydetmek ve kayıt sonrasında listeyi güncellemek gerekiyor.
Bu nedenle kullanıcı arayüzü ile veri işlemlerini birbirinden ayıran katmanlı bir yapı tercih ettim.
Dolap Hafızası’nın Google Play’e yayımlanma sürecinde yaşadığım diğer deneyimleri Google Play Uygulama Geliştirme Deneyimi: Kotlin ve Jetpack Compose ile Öğrendiğim 5 Şey yazımda ayrıca anlatmıştım.
Önce yapıyı basit bir şemayla açıklayalım.
KULLANICI
|
v
JETPACK COMPOSE (UI)
|
v
VIEWMODEL
|
v
REPOSITORY
|
v
ROOM DAO
|
v
SQLITE DATABASE Bu yapı veri okuma ve yazma işlemlerinde farklı yönlerde çalışır.
Kullanıcı yeni bir eşya kaydetmek istediğinde olay arayüzden ViewModel’e iletilir. ViewModel, ilgili Repository metodunu çağırır. Repository, DAO üzerinden yerel veritabanına kayıt yapar.
Veri değiştiğinde Room’un sağladığı Flow yeni listeyi yayınlayabilir. Repository bu veriyi ViewModel’e aktarır. ViewModel de ekranın durumunu günceller.
Son olarak Jetpack Compose yeni duruma göre ekranı yeniden oluşturur.
Böylece kullanıcı arayüzünün veritabanında hangi SQL sorgusunun çalıştırıldığını bilmesine gerek kalmaz.
| Katman | Sorumluluğu |
|---|---|
| Jetpack Compose | Kullanıcıya ekranı göstermek ve kullanıcı etkileşimlerini almak. |
| ViewModel | Ekran durumunu yönetmek, kullanıcı olaylarını işlemek. |
| Repository | Veri erişimini soyutlamak ve veri kaynaklarını yönetmek. |
| Room DAO | Yerel veritabanı sorgularını çalıştırmak. |
| Room Database | Yapılandırılmış verileri cihazda saklamak. |
Android’in resmi mimari rehberinde de Repository yapısı veri katmanının önemli bir parçası olarak ele alınıyor. Ayrıca offline-first uygulamalarda yerel veritabanının güvenilir veri kaynağı olarak kullanılması öneriliyor.
İlk olarak veritabanında saklanacak veriyi tanımlamak gerekiyor.
Dolap Hafızası’nda oda ve eşya kavramları önemli olduğu için örneğimizi bir eşyanın belirli bir odaya bağlı olması üzerinden kuracağız.
Gerçek bir projede oda, dolap ve saklama alanları ayrı tablolarla modellenebilir. Burada veri akışına odaklanmak için yalnızca oda kimliği ve eşya bilgilerini kullanıyoruz.
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "stored_items")
data class StoredItem(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val roomId: Long,
val name: String,
val note: String? = null,
val createdAt: Long =
System.currentTimeMillis()
) Bu örnekte:
id her eşya kaydının benzersiz kimliğidir.roomId eşyanın bağlı olduğu odayı temsil eder.name eşyanın adını tutar.note isteğe bağlı açıklamadır.createdAt oluşturulma zamanını saklar.@Entity anotasyonu ile bu modelin Room tarafından tablo olarak kullanılmasını sağlıyoruz.
@PrimaryKey(autoGenerate = true) ise yeni kayıtlar için otomatik kimlik oluşturulmasını sağlar.
Başlangıçta tüm verileri tek tabloda saklamak kolay görünebilir.
Ancak proje büyüdükçe oda, saklama alanı, eşya, fotoğraf ve kategori gibi kavramları ayrı modellerde yönetmek daha anlamlı hale gelebilir.
Örneğin aynı odadaki 50 eşyanın tamamında oda adını tekrar tekrar saklamak yerine ilgili oda kimliğini kullanmak veri tekrarını azaltabilir.
Daha gelişmiş bir modelde Room’un @ForeignKey ve @Relation gibi özellikleriyle tablolar arasındaki ilişkiler de yönetilebilir.
Room kullanırken SQL sorgularını Compose ekranlarına yazmıyoruz.
Bunun yerine DAO (Data Access Object) oluşturuyoruz.
DAO veritabanı erişimini arayüzün diğer bölümlerinden ayırır.
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface StoredItemDao {
@Query(
"SELECT * FROM stored_items " +
"WHERE roomId = :roomId " +
"ORDER BY createdAt DESC"
)
fun observeByRoom(
roomId: Long
): Flow<List<StoredItem>>
@Upsert
suspend fun upsert(
item: StoredItem
)
@Delete
suspend fun delete(
item: StoredItem
)
} Burada özellikle Flow kullanımı önemli.
Normal bir sorguda veriyi bir kez okuyup ekrana gönderebiliriz.
Ancak Flow<List<StoredItem>> kullanıldığında Room ilgili tablonun değişimlerini takip ederek sorgu sonucunu yeniden yayınlayabilir.
Böylece yeni bir eşya eklendiğinde ekranı manuel olarak yenilemeye gerek kalmaz.
Örneğin kullanıcı “Mutfak” odasına bir saklama kabı ekledi.
Veri kaydedildikten sonra ilgili Room sorgusu güncel listeyi üretir.
Yeni eşya kaydı
|
v
Room tablosu güncellenir
|
v
Flow yeni listeyi yayınlar
|
v
Repository
|
v
ViewModel
|
v
Compose ekranı güncellenir Bu yaklaşım kullanıcı arayüzünü veritabanı değişimlerine karşı duyarlı hale getirir.
Entity ve DAO tanımlandıktan sonra Room veritabanı sınıfı oluşturulur.
import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [StoredItem::class],
version = 1,
exportSchema = true
)
abstract class SampleDatabase : RoomDatabase() {
abstract fun storedItemDao(): StoredItemDao
} Buradaki version = 1 yalnızca örnek veritabanının başlangıç sürümüdür; Dolap Hafızası’nın gerçek veritabanı sürümünü temsil etmez.
exportSchema = true kullanıldığında Room’un şema çıktılarının oluşturulabilmesi için projede uygun şema konumlandırması da yapılandırılmalıdır.
Bu şema dosyalarını sürüm kontrolünde tutmak, ileride veritabanı migration işlemleri yaparken fayda sağlar.
val database = Room.databaseBuilder(
applicationContext,
SampleDatabase::class.java,
"sample_inventory.db"
).build()
val itemDao = database.storedItemDao() Production projelerinde veritabanı nesnesi genellikle uygulama kapsamında tek örnek olarak yönetilir.
Her Compose ekranı açıldığında yeniden veritabanı oluşturmak doğru bir yaklaşım değildir.
Bağımlılıkları yönetmek için manuel dependency injection veya Hilt gibi çözümler kullanılabilir. Hangi yöntemin seçileceği projenin büyüklüğüne ve ekibin ihtiyaçlarına bağlıdır.
Android uygulaması geliştirirken sık yapılan hatalardan biri ViewModel içerisinde doğrudan DAO kullanmak.
Basit projelerde bu yaklaşım çalışabilir. Ancak uygulama büyüdüğünde veri işlemlerinin farklı yerlerde tekrarlanmasına neden olabilir.
Repository katmanı veri erişimini merkezileştirmek için kullanılır.
Örneğin eşya kaydetme işleminde:
bu katman üzerinden organize edilebilir.
import kotlinx.coroutines.flow.Flow
interface ItemRepository {
fun observeItems(
roomId: Long
): Flow<List<StoredItem>>
suspend fun addItem(
roomId: Long,
name: String
)
} class RoomItemRepository(
private val dao: StoredItemDao
) : ItemRepository {
override fun observeItems(
roomId: Long
): Flow<List<StoredItem>> {
return dao.observeByRoom(roomId)
}
override suspend fun addItem(
roomId: Long,
name: String
) {
val cleanedName = name.trim()
require(cleanedName.isNotEmpty()) {
"Eşya adı boş olamaz"
}
val item = StoredItem(
roomId = roomId,
name = cleanedName
)
dao.upsert(item)
}
} Artık ViewModel, verinin Room’dan mı yoksa başka bir veri kaynağından mı geldiğini bilmek zorunda değil.
Bu ayrım özellikle test yazarken önem kazanıyor.
Gerçek Room veritabanını kullanmadan, sahte bir Repository ile ViewModel davranışlarını test etmek mümkün hale geliyor.
Jetpack Compose ekranları duruma bağlı çalışır.
Örneğin bir eşya listesi ekranında üç temel durum olabilir:
Liste boşsa kullanıcıya ayrıca uygun bir açıklama göstermek isteyebiliriz.
Bu durumları birbirinden bağımsız Boolean değişkenlerle yönetmek zamanla karmaşıklaşabilir.
Bu nedenle ekran durumunu tek bir model altında toplamak daha anlaşılır bir yaklaşım.
data class ItemsUiState(
val items: List<StoredItem> = emptyList(),
val isLoading: Boolean = true,
val errorMessage: String? = null
) Bu model, ekranın ihtiyaç duyduğu bilgileri tek nesnede toplar.
ViewModel, Repository’den gelen veriyi UI durumuna dönüştürür.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ItemsViewModel(
private val repository: ItemRepository,
private val roomId: Long
) : ViewModel() {
val uiState: StateFlow<ItemsUiState> =
repository.observeItems(roomId)
.map { items ->
ItemsUiState(
items = items,
isLoading = false
)
}
.catch {
emit(
ItemsUiState(
isLoading = false,
errorMessage =
"Eşyalar yüklenemedi"
)
)
}
.stateIn(
scope = viewModelScope,
started =
SharingStarted.WhileSubscribed(5_000),
initialValue = ItemsUiState()
)
fun addItem(name: String) {
viewModelScope.launch {
repository.addItem(
roomId = roomId,
name = name
)
}
}
} Buradaki kodun birkaç önemli noktası bulunuyor.
viewModelScope, ViewModel yaşam döngüsüne bağlı bir coroutine kapsamıdır.
ViewModel temizlendiğinde bu kapsamda çalışan coroutine işlemleri de iptal edilir.
Böylece ekran artık kullanılmıyorken bazı gereksiz işlerin devam etmesini önlemek kolaylaşır.
Repository’den gelen Flow verisini StateFlow haline getiriyoruz.
Böylece UI her zaman erişebileceği bir ekran durumuna sahip oluyor.
SharingStarted.WhileSubscribed(5_000), aktif abonelik kalmadığında belirli bir süre sonra upstream veri toplama işlemini durdurabilecek bir paylaşım stratejisidir.
Bu tür yaşam döngüsü kararları gereksiz kaynak kullanımını azaltmaya yardımcı olabilir.
Örnekte liste okuma hatasını catch ile ele aldık.
Ancak addItem() içindeki yazma hatalarının da üretim uygulamalarında ayrıca ele alınması gerekir.
Örneğin kullanıcı boş bir eşya adı gönderdiğinde Repository doğrulama hatası oluşturur. ViewModel bu hatayı yakalayıp kullanıcıya uygun bir mesaj gösterebilir.
Bu örnek mimari akışı göstermek için sadeleştirildiğinden form doğrulaması, kaydetme durumları ve tek seferlik kullanıcı mesajları ayrı tutulmuştur.
Artık veritabanı ve ViewModel tarafı hazır.
Sırada verileri Compose arayüzüne bağlamak var.
Android uygulamalarında Flow veya StateFlow değerlerini Compose içerisinde yaşam döngüsüne duyarlı biçimde toplamak için collectAsStateWithLifecycle() kullanılabilir.
Bu API, uygulama ekranının yaşam döngüsünü dikkate alarak veri akışının toplanmasını yönetir.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.runtime.getValue
@Composable
fun ItemsScreen(
viewModel: ItemsViewModel
) {
val uiState by
viewModel.uiState.collectAsStateWithLifecycle()
ItemsContent(uiState = uiState)
}
@Composable
fun ItemsContent(
uiState: ItemsUiState
) {
when {
uiState.isLoading -> {
CircularProgressIndicator()
}
uiState.errorMessage != null -> {
Text(
text = uiState.errorMessage
)
}
uiState.items.isEmpty() -> {
Text(
text = "Bu odada henüz eşya yok"
)
}
else -> {
LazyColumn {
items(
items = uiState.items,
key = { it.id }
) { item ->
Text(text = item.name)
}
}
}
}
} Burada iki ayrı Composable fonksiyon kullanmamızın nedeni önemli.
ItemsScreen ViewModel ile iletişim kuruyor.
ItemsContent ise yalnızca kendisine verilen UI durumunu çiziyor.
Bu ayrım sayesinde UI bileşenini gerçek ViewModel oluşturmadan test edebiliriz.
Ayrıca LazyColumn içerisinde key = { it.id } kullanmak, listede değişiklik olduğunda Compose’un öğeleri kararlı kimliklerle takip etmesine yardımcı olur.
Örneğin kullanıcı bir eşya kaydettiğinde:
ItemsScreen
|
v
ViewModel.addItem()
|
v
Repository.addItem()
|
v
Room DAO.upsert()
|
v
Veritabanı güncellenir
|
v
Flow yeni listeyi yayınlar
|
v
StateFlow güncellenir
|
v
Compose yeniden çizim yapar Bu akışta arayüzü güncellemek için ayrıca manuel bir “listeyi yeniden yükle” mekanizması kurmaya çoğu durumda gerek kalmaz.
Dolap Hafızası’nda temel kullanıcı verileri ağırlıklı olarak cihazda tutuluyor.
Bu yaklaşım benim için yalnızca teknik bir tercih değildi.
Uygulamanın temel amacı eşya kaydetmek ve daha sonra bu kayıtlara ulaşmak.
Bir kullanıcı internet bağlantısı olmadığında kendi dolabına kaydettiği eşyaları göremiyorsa uygulamanın temel işlevi zarar görüyor.
Bu nedenle local-first yaklaşım projeye oldukça uygun.
Offline-first, bir uygulamanın temel işlevlerinin tamamını veya kritik bir bölümünü internet bağlantısı olmadan gerçekleştirebilmesi anlamına geliyor.
Bu yaklaşımda yerel veritabanı güvenilir veri kaynağı olarak kullanılabilir.
Dolap Hafızası örneğinde:
Bu temel işlemlerin uzak bir sunucuya sürekli bağımlı olmaması önemli bir avantaj.
Burada sık karıştırılan bir ayrım bulunuyor.
Bir uygulamanın verileri yerel olarak saklaması, bu verilerin otomatik olarak başka cihazlara senkronize edildiği anlamına gelmiyor.
Çoklu cihaz senkronizasyonu gerekiyorsa:
gibi farklı problemler ortaya çıkıyor.
Bu nedenle offline-first mimariyi bulut senkronizasyonuyla aynı kavram olarak değerlendirmemek gerekiyor.
Google’ın resmi offline-first mimari rehberi de temel işlevlerde yerel veri kaynağının önemini vurguluyor.
Bir Android uygulamasında her bilgi ilişkisel veritabanında saklanmak zorunda değil.
Örneğin Dolap Hafızası’nda kullanıcı tercihleri için DataStore kullanıyorum.
Böylece temel eşya kayıtları ile uygulama ayarları farklı sorumluluklarda tutulabiliyor.
Örnek ayarlar:
Room, ilişkisel ve yapılandırılmış veriler için uygunken DataStore küçük ayarların ve tercihlerinin saklanmasında kullanılabilir.
Bu ayrım, veri modelinin gereksiz yere karmaşıklaşmasını önleyebilir.
Dolap Hafızası’nda hem WorkManager hem de Firebase Cloud Messaging kullanıyorum.
Ancak bu iki teknoloji birbirinin alternatifi değil.
WorkManager, uygulamanın arka planda güvenilir biçimde gerçekleştirmesi gereken ertelenebilir işler için geliştirilmiş Android Jetpack bileşenidir.
Örneğin yerel son kullanma tarihi kontrolleri veya belirli arka plan görevleri için uygun olabilir.
WorkManager’ın normal zamanlama mekanizması kesin saat garantisi vermez. Sistem pil optimizasyonları ve cihaz koşullarına bağlı olarak işin çalıştırılma zamanını değiştirebilir.
Bu nedenle tam belirlenmiş dakikada çalması gereken alarm türü ihtiyaçlarla ertelenebilir arka plan işlerini birbirinden ayırmak gerekiyor.
FCM ise bir sunucu veya Firebase tarafındaki mesaj altyapısından cihaza bildirim göndermek için kullanılır.
Örneğin kullanıcıları önemli bir uygulama duyurusu hakkında bilgilendirmek için tercih edilebilir.
| Teknoloji | Temel Kullanım |
|---|---|
| WorkManager | Cihaz üzerinde planlanan güvenilir arka plan görevleri. |
| Firebase Cloud Messaging | Uzak sistemden cihaza iletilen mesajlar. |
| Room | Yerel ve yapılandırılmış verileri saklama. |
| DataStore | Kullanıcı tercihleri ve küçük ayarları saklama. |
Ayrıca Android 13 ve sonrasında kullanıcıya bildirim göstermek için gerekli durumlarda POST_NOTIFICATIONS izninin çalışma zamanında istenmesi gerektiğini de unutmamak gerekiyor.
Geliştirme sürecinde başlangıçtaki veritabanı modeli zamanla değişebilir.
Örneğin ilk sürümde yalnızca eşya adı ve oda kimliği tutarken daha sonra:
eklemek isteyebilirsiniz.
Ancak veritabanı şemasını değiştirmek yalnızca Entity sınıfına yeni bir alan eklemekten ibaret değil.
Uygulamanız Google Play’de yayımlanmışsa eski sürümden güncelleme yapan kullanıcıların kayıtlarının korunması gerekiyor.
Veritabanı sürümünü artırıp gerekli migration yolunu tanımlamazsanız Room veritabanını açarken hata oluşturabilir.
Bu yüzden migration işlemlerinin sürüm geçişlerinden önce test edilmesi önemli.
Android’in resmi Room dokümantasyonu otomatik ve manuel migration seçeneklerini açıklıyor.
Room’un fallbackToDestructiveMigration() seçeneği bazı geliştirme ve test senaryolarında kolaylık sağlayabilir.
Ancak gerekli migration yolu bulunmadığında yerel tabloların yeniden oluşturulmasına ve kullanıcı verilerinin kaybolmasına neden olabilir.
Dolap Hafızası gibi kullanıcıların kişisel kayıtlarını sakladığı bir uygulamada bu önemli bir risk.
Yerel veriyi korumak, yeni özellik geliştirmek kadar önemlidir.
Katmanlı mimarinin en önemli avantajlarından biri test edilebilirlik.
Özellikle yazılım testi geçmişim açısından, kodun farklı katmanlarını bağımsız olarak doğrulayabilmek önemli bir konu.
Bir Android uygulamasında en az üç farklı seviyede test düşünülebilir:
Room için test sırasında bellek içi veritabanı oluşturulabilir.
Bu sayede gerçek kullanıcı verilerini etkilemeden kayıt ekleme, sorgulama ve silme işlemleri kontrol edilebilir.
Örneğin şu senaryoyu test edebiliriz:
Oda kimliği 7 olan alana “Dikiş seti” kaydedildiğinde, aynı odayı sorgulayan Flow bu kaydı döndürmeli.
Test sırasında kayıt yapıldıktan sonra sorgu sonucunun beklendiği gibi olup olmadığı doğrulanabilir.
Örneğimizde ItemsContent() fonksiyonu ViewModel’e bağımlı olmadığı için yalnızca UI state göndererek test edilebilir.
@get:Rule
val composeRule = createComposeRule()
@Test
fun itemNameIsVisible() {
val testItem = StoredItem(
id = 1L,
roomId = 7L,
name = "Dikiş seti"
)
composeRule.setContent {
ItemsContent(
uiState = ItemsUiState(
items = listOf(testItem),
isLoading = false
)
)
}
composeRule
.onNodeWithText("Dikiş seti")
.assertExists()
} Bu örnek, uygun Compose test bağımlılıkları ve JUnit importlarıyla kullanılabilecek bir UI testinin temelini gösteriyor.
Veritabanına veya gerçek Repository’ye bağlanmadan ilgili metnin ekranda bulunup bulunmadığını kontrol edebiliyoruz.
Bu yaklaşım özellikle UI değişikliklerinde regresyon testleri hazırlarken oldukça faydalı.
Katmanlı mimari kurmak tek başına temiz kod anlamına gelmiyor.
Katmanların sorumluluklarını doğru ayırmak gerekiyor.
Composable fonksiyonların içinde veritabanı erişimi başlatmak, UI ile veri katmanını birbirine sıkı bağlayabilir.
Veritabanı işlemleri Repository ve ilgili veri katmanında yönetilmeli.
ViewModel ekran durumunu yönetmek için kullanılıyor diye bütün veri erişimi ve iş kurallarının aynı dosyaya eklenmesi gerekmiyor.
İş mantığı büyüdükçe Repository veya gerektiğinde ayrı use case sınıfları oluşturmak daha anlaşılır bir tasarım sağlayabilir.
Her ekranın çok sayıda bağımsız durum değişkeniyle kontrol edilmesi tutarsız UI durumlarına neden olabilir.
Örneğin bir ekranda hem hata mesajı hem yükleme göstergesi hem de başarılı sonuç aynı anda görünebilir.
Tutarlı bir UI state modeli bu tür durumların yönetimini kolaylaştırır.
Room ve Flow kullanıldığı halde sürekli manuel sorgu çağırmak gereksiz karmaşıklık oluşturabilir.
Gözlemlenebilir sorguları kullanmak birçok durumda daha sade bir yaklaşım sağlar.
Her projede onlarca interface veya use case sınıfı oluşturmak zorunlu değil.
Özellikle küçük uygulamalarda fazla soyutlama kodun anlaşılmasını zorlaştırabilir.
Burada amaç mümkün olduğunca çok katman oluşturmak değil, sorumlulukları doğru ayırmak.
Yazılım geliştirirken başlangıçta bütün ihtiyaçları kusursuz tahmin etmek mümkün değil.
Dolap Hafızası’nda da yeni özellikler geldikçe farklı ihtiyaçlar ortaya çıktı.
Örneğin:
Bunların her biri farklı sorumluluklara sahip.
Room bütün problemleri çözmez.
ViewModel bütün işlemleri gerçekleştirmez.
Compose yalnızca kullanıcı arayüzü tarafına odaklanır.
WorkManager arka plan işlemlerinin bir bölümünü üstlenir.
DataStore tercihler için kullanılabilir.
Firebase ise uzak bildirim altyapısının parçası olabilir.
İyi mimarinin temel amacı bu bileşenleri tek bir büyük dosyada birleştirmek değil, birbirleriyle anlaşılır sınırlar üzerinden çalıştırmaktır.
Bir uygulama yalnızca geliştiricinin kendi telefonunda çalışırken bazı problemler fark edilmeyebilir.
Ancak Google Play’e yayımlandığında farklı Android sürümleri, ekran boyutları ve cihaz davranışlarıyla karşılaşır.
Bu noktada mimari kararlar doğrudan bakım sürecini etkiler.
Örneğin eşya kayıtlarının gösterilmesinde bir hata varsa:
ayrı ayrı kontrol edilebilir.
Bu, hatanın nedenini bulmayı kolaylaştırabilir.
Dolap Hafızası’nın geliştirme sürecinde Android SDK gereksinimleri de önemli bir yer tuttu.
Google Play’in 31 Ağustos 2026 tarihinden itibaren uyguladığı şartlara göre yeni standart Android uygulamalarının ve güncellemelerinin Android 16, yani API 36 veya üzerini hedeflemesi gerekiyor.
Ancak targetSdk değerini artırmak tek başına yeterli değil.
Yeni Android sürümlerindeki davranış değişikliklerini, bildirim izinlerini, arka plan işlem kısıtlamalarını ve cihaz uyumluluğunu da test etmek gerekiyor.
Android dağıtım ve Google Play politikalarıyla ilgili ayrıca Android Geliştirici Doğrulaması 2027 yazımı inceleyebilirsiniz.
Bu mimariyi kullanmak için mutlaka büyük bir uygulama geliştirmeniz gerekmiyor.
Basit bir alışveriş listesi, stok takip uygulaması veya not uygulaması üzerinde de benzer bir yapı kurulabilir.
Başlangıç için şu geliştirme sırasını öneriyorum:
Bu sırayla ilerlemek, her aşamada hangi sorumluluğun hangi katmana ait olduğunu daha net görmenizi sağlar.
Bu projede kullandığım teknolojilerden öğrendiğim önemli noktalardan bazıları şunlar:
Compose ekranlarının temel sorumluluğu veriyi göstermek ve kullanıcı olaylarını iletmek olmalı.
Yeni veri kaynakları veya iş kuralları eklendiğinde değişiklikleri kontrol etmek kolaylaşır.
Veri değiştiğinde kullanıcı arayüzünün buna uygun şekilde güncellenmesi daha sade hale geliyor.
Dolap Hafızası gibi kullanıcının kendi kayıtlarına internet olmadan erişmesi gereken bir uygulamada bu yaklaşım anlamlı.
Uygulama yayımlandıktan sonra kullanıcı verilerini korumak öncelikli hale geliyor.
Önemli olan doğru soyutlamaları ihtiyaç duyulan yerde kullanmak.
Jetpack Compose MVVM Room mimarisi, Kotlin ile Android uygulaması geliştirirken veritabanı, iş mantığı ve kullanıcı arayüzünü daha düzenli yönetmek için kullanılabilecek etkili bir çözüm.
Dolap Hafızası’nda edindiğim deneyimler bana, bir uygulamanın yalnızca çalışmasının yeterli olmadığını tekrar gösterdi.
Yeni özellikler eklenirken mevcut kodun anlaşılabilir olması, verinin korunması, hataların kolay tespit edilmesi ve testlerin sürdürülebilmesi de önemli.
Room ile yerel verileri saklamak, Repository üzerinden erişimi yönetmek, ViewModel ile ekran durumunu üretmek ve Jetpack Compose ile bu durumu göstermek bir araya geldiğinde oldukça anlaşılır bir veri akışı kurulabiliyor.
Elbette tek bir mimari bütün projeler için ideal değil.
Ancak gerçek kullanıcıların kullandığı ve zaman içerisinde gelişmeye devam eden Android uygulamalarında sorumlulukları ayırmak uzun vadede önemli avantajlar sağlayabilir.
Bu projeden çıkardığım en önemli sonuç şu:
İyi bir Android mimarisi yalnızca bugünün özelliklerini çalıştırmaz; yarın eklenecek özellikleri de yönetilebilir hale getirmeye yardımcı olur.
Dolap Hafızası uygulamasını incelemek isteyenler için:
Proje hakkında daha fazla bilgi:
Jetpack Compose kullanıcı arayüzünü, ViewModel ekran durumunu, Repository veri erişimini ve Room yerel veritabanını yönetir. Bu bileşenler birlikte kullanıldığında katmanlı bir Android mimarisi kurulabilir.
Evet. Room DAO üzerinden Flow ile veri yayınlanabilir. ViewModel bu akışı StateFlow’a dönüştürerek Compose ekranının güncel verileri izlemesini sağlayabilir.
Hayır. MVVM yaygın bir yaklaşımdır ancak her proje için zorunlu değildir. Uygulamanın karmaşıklığına ve gereksinimlerine göre farklı mimari tasarımlar da kullanılabilir.
Room yapılandırılmış ve ilişkisel verileri SQLite üzerinde yönetmek için kullanılır. DataStore ise küçük kullanıcı tercihlerini ve ayarları saklamak için uygundur.
StateFlow güncel bir değeri tutan ve değişiklikleri abonelerine ileten bir akış türüdür. Android’de ViewModel tarafından üretilen ekran durumunu Compose ile paylaşmak için kullanılabilir.
Hayır. Room cihaz üzerindeki SQLite veritabanıyla çalışır. Verilerin uzak bir sunucudan alınması veya senkronize edilmesi ayrı bir gereksinimdir.
Hayır. WorkManager, ertelenebilir arka plan işlemlerinin güvenilir şekilde yürütülmesi için kullanılır. Firebase Cloud Messaging ise uzak sistemlerden cihazlara mesaj iletilmesini sağlar.
Teknik olarak zorunlu değil. Ancak veri erişimi karmaşıklaştığında, farklı veri kaynakları bulunduğunda veya test edilebilirlik önemli olduğunda Repository kullanımı faydalı olabilir.
Migration, uygulama güncellemelerinde veritabanı şeması değişirken mevcut kullanıcı verilerinin korunmasını sağlar. Yanlış veya eksik migration işlemleri uygulamanın açılmasını engelleyebilir veya veri kaybına neden olabilir.
Evet. Compose UI Test API’leri sayesinde ekrandaki bileşenler bulunabilir, kullanıcı etkileşimleri simüle edilebilir ve beklenen sonuçlar doğrulanabilir.
Yerel Android uygulaması geliştirme, genel amaçlı bir mobil uygulama hazırlamaktan farklı problemler ortaya çıkarabiliyor. Çünkü…
Google Play uygulama geliştirme deneyimi, yalnızca Android Studio'da çalışan bir uygulama hazırlayıp AAB dosyasını Play…
.NET 11 yenilikleri, Microsoft'un yeni nesil uygulama geliştirme platformunda C# dilinden ASP.NET Core'a, Entity Framework…
Android geliştirici doğrulaması, Google'ın Android ekosisteminde uygulama dağıtan geliştiricilerin kimliğini ve uygulama paketlerinin sahipliğini doğrulamak…
GPT-6.1 Terra, OpenAI'ın hız, yetenek ve maliyet dengesine odaklanan yeni modeli olarak 26 Ekim 2026…
OpenAI, uzun süredir birçok production uygulamasının temelinde kullanılan GPT-4o ve GPT-4.1 ailesindeki belirli API modellerini…