RoomDB Nedir?
RoomDB-MVVM-Databinding
Herkese merhaba,
Bu yazımda sizlere RoomDB’nin Mvvm ve databinding ile kullanımını anlatacağım. Peki RoomDB nedir?
RDB Android JetPack’in bir ögesi. Ve RoomDB
- Uygulamanızda SQLiteDatabase nesneleriyle çalışmayı kolaylaştırır.
- Kod miktarını azaltır ve derleme zamanında SQL sorgularını doğrular.
Dolayısıyla işimizi de çok kolaylaştırır.
Basit olarak matığı şekildeki gibidir. UI’den verileri VM’e aktarılır. Oradan da Network ve Local repo için Room’a ulaşılır. Ancak burada DAO dediğimiz bir obje çıkıyor karşımıza. Data Accessing Object. Bu katman da aslında kendi içinde bir mimari yapıdır. Yazıda biraz daha detaylı bahsederiz mantığından. Ancak SQLite ile konuşan şey burada aslında DAO’dur. DAO’ da bir livedata döndürür ve böylece viewmodel’ımıza veri taşınmış olur. Çok soyut kaldı hadi başlayalım!
Basit bir listeleme app’i yapalım!
Basit olarak isim soyisim ve yaşımızı girdiğimiz bunu local’de saklayan bir app yapalım.
1.Bağlılıklar
Module: app içerisine plugin eklentisini yapıyoruz.
apply plugin: 'kotlin-kapt'
Uyarıları engellemek için packaging Options’da AFU modülünü çıkarıyoruz.
android {
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
Dependencies içine aşağıdaki kütüphaneleri ekleyelim.
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
Project build.gradle’a kütüphane versiyonlarına girelim ve işlem tamam.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
2.Entity Kurulumu
Entity de nedir?
Olabildiğince sade anlatacağım. Eğer bir sınıfın başına
yazarsanız , benim az sonra yazacağım class aslında bir tablo kardeşim demiş olursunuz ideye, hepsi bu kadar aslında. Ufak bir iki örnek yapacağım burada ancak SQL’in içinde boğulmadan mantığı kurarsak bizim için yeterli.
Basit Entity’mizi kuralım.
Çok basit bir Entity örneği kurduk aslında. “@Entity” diyerek bunun bir tablo olduğunu, ve tablo adını yazdık. Data class’ımızı girelim şimdi de. User dataclass’ımızda istersek bir primary key atabiliyoruz. Ve SQL’in bir özelliği olarak da bunu şekildeki gibi otomatik artan bir şekilde kullanabiliyoruz.
“@ColumnInfo” annotation’ı aslında adı üstünde sütunun bilgisi. Sütunumuzun adı “first_name” olsun ve string bir değer alsın demişiz. Buna default_değer vs de verebiliyoruz. (SQL’ın sınırları yok.)
“@Nullable” annotation’ını kullanmayacağız ama sutünun illa dolu olması gerekmiyodur isterseniz ekleyebilirsiniz veya “@NotNull” annotation’ınını…
Peki ya sonra? Böyle bir şarkı mı vardı?
Buraya kadar aslında database’imizin içindeki tablonun özelliklerini belirtmekten başka hiçbir şey yapmadık.
O zaman DAO dediğimiz mevzuya artık yavaş yavaş gelebiliriz.
3.DAO
Şuan için oluşturacağımız sınıf bir DAO, yani bir veri erişim nesnesi. Ve bizim SQLite ile konuşacağımız yer işte tam da burası.
Bir kaç spoiler zaten aldık ve bir önceki mantık ile çok benzer aslında. “@Dao” gördüğümüz yer bu interface’in iletişimi kuran interface olduğunu belirtir. Ee iletişim kurulacaksa bir kaç iletişim metodu da ekleyelim buraya..
“@Query” ile sorgumuzu attık ve users tablosundaki bütün verileri first_name’e göre alfabetik şekilde istedik. Cevabımız LiveData türünden liste olacak. Listenin tipide en başta oluşturduğumuz dataclass tipinde.
“@Insert” ile yapılacak şey ortada veri ekleme, ancak onConflict diye bir özellik görüyoruz bu nedir? Hemen onConflictin üst sınıfına bakalım neymiş?
“@Update” ile de güncelleme işlerini yapabiliyoruz. (Uygulamamızda kullanmadık.)
public @interface OnConflictStrategy {
/**
* OnConflict strategy constant to replace the old data and continue the transaction.
* <p>
* An {@link Insert} DAO method that returns the inserted rows ids will never return -1 since
* this strategy will always insert a row even if there is a conflict.
*/
int REPLACE = 1;
/**
* OnConflict strategy constant to rollback the transaction.
*
* @deprecated Does not work with Android's current SQLite bindings. Use {@link #ABORT} to
* roll back the transaction.
*/
@Deprecated
int ROLLBACK = 2;
/**
* OnConflict strategy constant to abort the transaction. <em>The transaction is rolled
* back.</em>
*/
int ABORT = 3;
/**
* OnConflict strategy constant to fail the transaction.
*
* @deprecated Does not work as expected. The transaction is rolled back. Use {@link #ABORT}.
*/
@Deprecated
int FAIL = 4;
/**
* OnConflict strategy constant to ignore the conflict.
* <p>
* An {@link Insert} DAO method that returns the inserted rows ids will return -1 for rows
* that are not inserted since this strategy will ignore the row if there is a conflict.
*/
int IGNORE = 5;
Gördüğümüz gibi onConflict verileri işleme stratejisinden başka bir şey değil. Biz Ignore seçmişiz yani verilerimiz tablomuzda var mı yok mu bakmadan ekle. Bunun yerine replace, rollback vb seçenekleri de kullanabilirsiniz.
Diğer üç query‘miz de isim, soyisim ve yaş kolonlarını çekip liste döndüren querylerimiz.
Son Queryimiz de tabloyu silmek için bir parametremiz. Peki ya biz gelişmiş bir sorgu atmak istersek o zaman ne olacak derseniz sizin için bir kod satırı bırakıp devam edeceğim.
Örneğin bu satırda getallUserswithnameis metodu içerisinde gelen veriyi Query’nin içine bu şekilde dahil edebilirsiniz.
4.Database katmanı
“abstract fun userDao(): UserDao” metodunu abstract class’ımızı içine daha önceden kurduğumuz Dao interface’ini bağlayalım ki Dao’nun metodlarına erişebilelim.
“@Database” annotation’ı o sınıfı bir database olarak işaretler. Ve parametre olarak daha önce oluşturduğumuz entity’lerin listesini , database versiyonunu ve isteğe bağlı exportSchema boolean değeri alır. Aldığı her entity database’e kendi table’ını döndürür. Exportschema ise RoomDB üzerindeki güncellemeler değiştirmeler için kullanılan bir database’in şeması. Ayrıca isterseniz compile time’da JSON olarak bu şemayı çıkartıp saklayabilirsiniz.
Basit olarak Roomda bir Migration
sınıf, belirli bir sürümden diğerine geçerken gerçekleştirilmesi gereken eylemleri tanımlar. RoomDB migrate işlemleri için buraya ilgili kaynağı bırakıyorum.
https://developer.android.com/training/data-storage/room/migrating-db-versions
RoomDataBase’den extend ettiğimiz bu abstract sınıf içerisinde öncelikle database class’imizi kurduk. Ve database instance’ımızın da birden fazla yaratılmasını önlemek için Singleton yapısında kurduk. fun getDatabase fonksiyonu ile database’imize erişeceğiz. Ve bu fonksiyon içerisine “inline synchronized” fonksiyonunu kullanıyoruz ki database’imiz ilgili işlemi yapmadan başka call alırsa ikisini de aynı anda çağırmaya çalışmasın. Buna concurrency yani eşzamanlılık denir. Dolayısıyla synchronized fonksiyonu eş zamanlı threadlerin bir obje üzerinde değişiklik yapmasını engeller.
❗❗❗
Burada korkmamız gereken bir method fallbackToDestructiveMigration() metodudur.
❗ fallbackToDestructiveMigration()
Bu metod roomdb üzerinde herhangi bir versiyon yükseltme protokolü dediğimiz belirteçleri Migration objelerle bulamamışsa ilgili database’in tüm tablolarındaki verileri siler!!!!!! Dolayısıyla gerçek bir proje senaryosunda bu metod kullanılmaz.
Devam edelim. addCallback metodu ile de bir callback ekleyeceğiz. Callback nedir? Callback basit olarak bize bir database’i kurup, yıkıp kurup veya açan bir metod. Zaten add diyerek kendi Callback’imizi verdik. Ve bu Callback bizim fonksyiondan aldığımız coroutine scope’ ile çalışır. RoomDB’nin main thread üzerinde çalışmaması kısmı işte burada devreye giriyor. Her neyse biz bu callback içinde database kurulduğunda veri ekleyelim dedik ve bu scope üzerinde çalışacak bir veri ekleme metodu girdik. populateDatabase metodu ile önce veritabanındaki verileri silip tekrar girdik. Bu kısımı normalde Override etmenize gerek yok bu tarz işlemlerin hepsi normalde Dao katmanında yapılır. Biz database açılır açılmaz veriler eklensin istiyoruz ki bu verileri daha sonra vm-databinding ile ekranımızda görebilelim.
Bitti mi derseniz? Daha başlıyoruz :) Ama en azından database’imizi kurabilceğimiz yapıyı hazırladık artık.
Şuraya bir görsel bırakıyım da içimiz şişmesin :)
Artık database ve onun içinde dao, datadabase ve entity katmanlarımız hazır. O zaman sıra repomuzda :)
5.Repository
Konumuz biraz daha RoomDB olduğu için artık biraz daha gaza basabiliriz. Gene de oldukça açıklayıcı olmaya çalışacağım. Repomuzun adını UserReporsitory koyduk ancak bu network katmanı olmayan bir repo. Şuanda kafamız karışmasın RoomDB ve uzak sunucu repoları ayrıymış gibi düşünelim. Burada ihtiyacımız olan tek şey bir UserDao bunun üzerinden istediğimiz sorguları atabilir, istediğimiz metodları çalıştırabiliriz. Ve aradaki herhangi bir objeyle uğraşmamış olduk. Ve repomuza gelen UserDao katmanıyla getAllUsers metodunu çalıştırıp ( ki bu metod bir livedata dönüyordu.) bunu bir liste tipinde livedataya eşitledik. Artık viewmodel ile repomuzla yani modelimizle konuşabiliriz.
6.View Model
private UserRepository yarattık. Allstore adındaki değişkene Livedatamızı ilettik, ve bu sınıfın bir yerde adı geçer geçmez çalışacak olan init bloğunda bu değişkenimizin değerini aldık.
Repo alt katmanındaki suspend insert metodunu kullanmak için de insert metodu ekleyip bunu repomuza bağladık. Bitti bu kadar :)
Hadi bakalım şimdi kaldı bindingi :)
7.DataBinding
Module:app içerisinde dataBindingimizi aktif edelim.
dataBinding {
enabled = true
}
Bir xml’ oluşturalım <layout> tagları içine tasarımımızı yerleştirelim.
Databinding kısmına sonra geleceğiz. Ee madem recyclerview’ımız var o zaman buna bir de adapter yazalım.
Adapter class’ımızı kurarken internal keyword’unun aynı modül içinde erişilebilir olduğunu hatırlatıp, kuralım. Daha sonra adapter class’ının içine set metodunu kendimiz yazacağımız user tipinde bir liste tanımlayalım.
inner class’ımızın içinde baseadapter içinde oluşan itemrecyclerview xml’ine verdiğimiz datayı set eden bir fonksiyon yazalım. Ve bu içine gelen datayı basitçe customitemlist’teki <variable> tagları içindeki dataya eşitlesin. Eğer eşitlenecek nesnenin tipi belli değilse reflectiondan yardım alınarak binding’deki data da şu şekilde eşitlenebilir.
binding.setVariable(BR.data,data)
Hemen şuraya da recyclerview_item.xml’ini oluşturalım.
8. Ve son :)
Son olarak mainactivity içerisinde verilerimizi observe’leyip adapterımızı set edelim.
https://gist.github.com/inancyillmaz/13acac09359e5ef634f4b584751bf787
ss:
Github linki: https://github.com/inancyillmaz/ROOM-MVVM-DATABINDING
Faydalanılan kaynaklar :
https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0
Gökhan ÖZTÜRK’e ve Burak Karabulut’a katkılarından dolayı teşekkür ederim.