Android Room with a View - Kotlin

Architecture Components'ın amacı, uygulama mimarisi hakkında rehberlik sağlamaktır. Yaşam döngüsü yönetimi ve veri kalıcılığı gibi yaygın görevler için kitaplıklar içerir. Mimari bileşenler, uygulamanızı daha az standart kodla sağlam, test edilebilir ve bakımı kolay bir şekilde yapılandırmanıza yardımcı olur. Architecture Component kitaplıkları, Android Jetpack'in bir parçasıdır.

Bu, codelab'in Kotlin sürümüdür. Java programlama dilindeki sürümü burada bulabilirsiniz.

Bu codelab'i uygularken herhangi bir sorunla (kod hataları, dilbilgisi hataları, net olmayan ifadeler vb.) karşılaşırsanız lütfen codelab'in sol alt köşesindeki Hata bildir bağlantısını kullanarak sorunu bildirin.

Ön koşullar

Özellikle Kotlin, nesne yönelimli tasarım kavramları ve Android geliştirmenin temelleri hakkında bilgi sahibi olmanız gerekir:

Ayrıca, verileri kullanıcı arayüzünden ayıran MVP veya MVC gibi yazılım mimarisi kalıplarına aşina olmak da faydalıdır. Bu codelab'de, Uygulama mimarisi kılavuzunda tanımlanan mimari uygulanmaktadır.

Bu codelab, Android Architecture Components'a odaklanmaktadır. Konuyla alakasız kavramlar ve kodlar, yalnızca kopyalayıp yapıştırmanız için paylaşılmıştır.

Kotlin hakkında bilginiz yoksa bu codelab'in Java programlama dilinde hazırlanmış sürümüne buradan ulaşabilirsiniz.

Yapacaklarınız

Bu codelab'de, Architecture Components Room, ViewModel ve LiveData'yı kullanarak bir uygulama tasarlamayı ve oluşturmayı öğrenecek, ayrıca aşağıdaki işlemleri yapan bir uygulama geliştireceksiniz:

  • Android Architecture Components'ı kullanarak önerilen mimarimizi uygular.
  • Verileri almak ve kaydetmek için bir veritabanıyla çalışır ve veritabanını bazı kelimelerle önceden doldurur.
  • RecyclerView içindeki tüm kelimeleri MainActivity olarak gösterir.
  • Kullanıcı + düğmesine dokunduğunda ikinci bir etkinlik açar. Kullanıcı bir kelime girdiğinde, kelimeyi veritabanına ve listeye ekler.

Uygulama basit olsa da üzerine eklemeler yapabileceğiniz bir şablon olarak kullanılabilecek kadar karmaşıktır. Önizleme:

İhtiyacınız olanlar

  • Android Studio 3.0 veya sonraki bir sürüm ve bu sürümün nasıl kullanılacağı hakkında bilgi. Android Studio'nun, SDK'nızın ve Gradle'ınızın güncellendiğinden emin olun.
  • Android cihaz veya emülatör.

Bu codelab, uygulamanın tamamını oluşturmak için ihtiyacınız olan tüm kodları sağlar.

Architecture Components'ı kullanmak ve önerilen mimariyi uygulamak için birçok adım gerekir. En önemli şey, neler olup bittiğine dair bir zihinsel model oluşturmak, parçaların nasıl bir araya geldiğini ve verilerin nasıl aktığını anlamaktır. Bu codelab'i tamamlarken kodu kopyalayıp yapıştırmak yerine, kodu anlamaya çalışın.

Terminolojiyi tanıtmak için Architecture Components ve bunların birlikte nasıl çalıştığı hakkında kısa bir giriş yapalım. Bu codelab'in, bileşenlerin bir alt kümesi olan LiveData, ViewModel ve Room'a odaklandığını unutmayın. Her bileşen, kullanıldıkça daha ayrıntılı olarak açıklanır.

Bu şemada, mimarinin temel bir şekli gösterilmektedir:

Varlık: Room ile çalışırken bir veritabanı tablosunu açıklayan, ek açıklamalı sınıf.

SQLite veritabanı: Cihazda depolama. Room kalıcılık kitaplığı, bu veritabanını sizin için oluşturur ve korur.

DAO: Veri erişimi nesnesi. SQL sorgularının işlevlerle eşlemesi. DAO kullandığınızda yöntemleri çağırırsınız ve geri kalan işlemleri Room halleder.

Room veritabanı: Veritabanı çalışmalarını basitleştirir ve temel SQLite veritabanına erişim noktası olarak hizmet verir (SQLiteOpenHelper) gizler). Room veritabanı, SQLite veritabanına sorgu göndermek için DAO'yu kullanır.

Depo: Öncelikli olarak birden fazla veri kaynağını yönetmek için oluşturduğunuz bir sınıf.

ViewModel: Depo (veri) ile kullanıcı arayüzü arasında bir iletişim merkezi görevi görür. Kullanıcı arayüzünün artık verilerin kaynağıyla ilgili endişelenmesi gerekmez. ViewModel örnekleri, Etkinlik/Parça yeniden oluşturulmasından etkilenmez.

LiveData: Gözlemlenebilen bir veri tutucu sınıf. Verilerin en son sürümünü her zaman tutar/önbelleğe alır ve veriler değiştiğinde gözlemcilerini bilgilendirir. LiveData, yaşam döngüsünün farkındadır. Kullanıcı arayüzü bileşenleri yalnızca ilgili verileri gözlemler ve gözlemeyi durdurmaz veya devam ettirmez. LiveData, gözlemleme sırasında ilgili yaşam döngüsü durumu değişikliklerinin farkında olduğundan tüm bunları otomatik olarak yönetir.

RoomWordSample mimarisine genel bakış

Aşağıdaki şemada uygulamanın tüm parçaları gösterilmektedir. Kapsayan kutuların her biri (SQLite veritabanı hariç) oluşturacağınız bir sınıfı temsil eder.

  1. Android Studio'yu açın ve Start a new Android Studio project'i (Yeni bir Android Studio projesi başlat) tıklayın.
  2. Yeni Proje Oluştur penceresinde Empty Activity'yi (Boş Etkinlik) seçin ve Next'i (İleri) tıklayın.
  3. Sonraki ekranda uygulamayı RoomWordSample olarak adlandırın ve Finish'i (Bitir) tıklayın.

Ardından, bileşen kitaplıklarını Gradle dosyalarınıza eklemeniz gerekir.

  1. Android Studio'da Projeler sekmesini tıklayın ve Gradle Komut Dosyaları klasörünü genişletin.

build.gradle (Modül: uygulama) öğesini açın.

  1. kapt Annotation processor Kotlin eklentisini, build.gradle (Module: app) dosyanızın üst kısmında tanımlanan diğer eklentilerden sonra ekleyerek uygulayın.
apply plugin: 'kotlin-kapt'
  1. Pakete atomik işlevler modülünün dahil edilmemesi ve uyarıların önlenmesi için android bloğunun içine packagingOptions bloğunu ekleyin.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. Aşağıdaki kodu dependencies bloğunun sonuna ekleyin.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$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"
  1. build.gradle (Project: RoomWordsSample) dosyanızda, aşağıdaki kodda verildiği gibi dosyanın sonuna sürüm numaralarını ekleyin.
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

Bu uygulama için veriler kelimelerdir ve bu değerleri tutmak için basit bir tabloya ihtiyacınız vardır:

Room, Entity aracılığıyla tablolar oluşturmanıza olanak tanır. Hemen başlayalım.

  1. Word veri sınıfını içeren Word adlı yeni bir Kotlin sınıfı dosyası oluşturun.
    Bu sınıf, kelimeleriniz için Entity'yi (SQLite tablosunu temsil eder) açıklar. Sınıftaki her özellik, tablodaki bir sütunu temsil eder. Room, hem tablo oluşturmak hem de veritabanındaki satırlardan nesneler oluşturmak için bu özellikleri kullanır.

Kod:

data class Word(val word: String)

Word sınıfının bir Room veritabanı için anlamlı olması amacıyla bu sınıfa açıklama eklemeniz gerekir. Ek açıklamalar, bu sınıftaki her bir bölümün veritabanındaki bir girişle nasıl ilişkili olduğunu tanımlar. Room, kod oluşturmak için bu bilgileri kullanır.

Ek açıklamaları yapıştırmak yerine kendiniz yazarsanız Android Studio, ek açıklama sınıflarını otomatik olarak içe aktarır.

  1. Word sınıfınızı bu kodda gösterildiği gibi ek açıklamalarla güncelleyin:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

Bu ek açıklamaların ne işe yaradığına bakalım:

  • @Entity(tableName = "word_table")
    Her @Entity sınıfı bir SQLite tablosunu temsil eder. Sınıf bildiriminizin bir öğe olduğunu belirtmek için sınıf bildirimine açıklama ekleyin. Sınıfın adından farklı olmasını istiyorsanız tablonun adını belirtebilirsiniz. Bu işlem, tabloya "word_table" adını verir.
  • @PrimaryKey
    Her varlığın birincil anahtarı olmalıdır. İşleri basitleştirmek için her kelime kendi birincil anahtarı olarak işlev görür.
  • @ColumnInfo(name = "word")
    Tablodaki sütunun adının üye değişkeninin adından farklı olmasını istiyorsanız sütunun adını belirtir. Bu işlem, sütunu "word" olarak adlandırır.
  • Veritabanında depolanan her özelliğin, Kotlin'in varsayılan değeri olan herkese açık görünürlüğe sahip olması gerekir.

Açıklamaların tam listesini Oda paketi özeti referansında bulabilirsiniz.

DAO nedir?

DAO'da (veri erişim nesnesi) SQL sorgularını belirtir ve bunları yöntem çağrılarıyla ilişkilendirirsiniz. Derleyici, SQL'i kontrol eder ve @Insert gibi yaygın sorgular için kolaylık ek açıklamalarından sorgular oluşturur. Room, kodunuz için temiz bir API oluşturmak üzere DAO'yu kullanır.

DAO, bir arayüz veya soyut sınıf olmalıdır.

Varsayılan olarak tüm sorgular ayrı bir ileti dizisinde yürütülmelidir.

Room, coroutine desteği sunar. Bu sayede sorgularınız suspend değiştiricisiyle açıklama eklenerek bir coroutine'den veya başka bir askıya alma işlevinden çağrılabilir.

DAO'yu uygulama

Şu sorguları sağlayan bir DAO yazalım:

  • Tüm kelimeleri alfabetik olarak sıralama
  • Kelime ekleme
  • Tüm kelimeleri silme
  1. WordDao adlı yeni bir Kotlin sınıfı dosyası oluşturun.
  2. Aşağıdaki kodu kopyalayıp WordDao dosyasına yapıştırın ve derlenmesi için içe aktarma işlemlerini gerektiği gibi düzeltin.
@Dao
interface WordDao {

    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAlphabetizedWords(): List<Word>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)

    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
}

Şimdi bu süreci adım adım inceleyelim:

  • WordDao bir arayüzdür. DAO'lar arayüz veya soyut sınıf olmalıdır.
  • @Dao ek açıklaması, bunu Room için bir DAO sınıfı olarak tanımlar.
  • suspend fun insert(word: Word) : Bir kelime eklemek için askıya alma işlevi bildirir.
  • @Insert ek açıklaması, herhangi bir SQL sağlamanız gerekmeyen özel bir DAO yöntemi ek açıklamasıdır. (Satırları silmek ve güncellemek için @Delete ve @Update ek açıklamaları da vardır ancak bu uygulamada bunları kullanmıyorsunuz.)
  • onConflict = OnConflictStrategy.IGNORE: Seçilen onConflict stratejisi, listede zaten bulunan bir kelimeyle tamamen aynı olan yeni bir kelimeyi yoksayar. Kullanılabilir çakışma stratejileri hakkında daha fazla bilgi edinmek için belgeleri inceleyin.
  • suspend fun deleteAll(): Tüm kelimeleri silmek için askıya alma işlevi bildirir.
  • Birden fazla öğeyi silmek için kolaylık sağlayan bir açıklama olmadığından, genel @Query ile açıklama eklenir.
  • @Query("DELETE FROM word_table"): @Query, karmaşık okuma sorgularına ve diğer işlemlere olanak tanımak için ek açıklamaya dize parametresi olarak bir SQL sorgusu sağlamanızı gerektirir.
  • fun getAlphabetizedWords(): List<Word>: Tüm kelimeleri alıp Words List döndürme yöntemi.
  • @Query("SELECT * from word_table ORDER BY word ASC"): Artan düzende sıralanmış bir kelime listesi döndüren sorgu.

Veriler değiştiğinde genellikle güncellenen verileri kullanıcı arayüzünde göstermek gibi bir işlem yapmak istersiniz. Bu nedenle, verileri gözlemlemeniz gerekir. Böylece değiştiğinde tepki verebilirsiniz.

Verilerin nasıl depolandığına bağlı olarak bu işlem zor olabilir. Uygulamanızın birden fazla bileşenindeki verilerde yapılan değişiklikleri gözlemlemek, bileşenler arasında açık ve katı bağımlılık yolları oluşturabilir. Bu durum, diğer şeylerin yanı sıra test etme ve hata ayıklama işlemlerini zorlaştırır.

LiveData, veri gözlemi için bir yaşam döngüsü kitaplığı sınıfı olarak bu sorunu çözer. Yöntem açıklamanızda LiveData türünde bir dönüş değeri kullanın. Room, veritabanı güncellendiğinde LiveData değerini güncellemek için gereken tüm kodu oluşturur.

WordDao içinde, döndürülen List<Word> değerinin LiveData ile sarmalanması için getAlphabetizedWords() yöntem imzasını değiştirin.

   @Query("SELECT * from word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): LiveData<List<Word>>

Bu codelab'in ilerleyen bölümlerinde, MainActivity içinde Observer aracılığıyla veri değişikliklerini izleyeceksiniz.

Room veritabanı nedir?

  • Room, SQLite veritabanının üzerinde yer alan bir veritabanı katmanıdır.
  • Room, daha önce SQLiteOpenHelper ile yaptığınız sıradan görevleri halleder.
  • Room, veritabanına sorgu göndermek için DAO'yu kullanır.
  • Varsayılan olarak, Room, kullanıcı arayüzü performansının düşük olmasını önlemek için ana iş parçacığında sorgu göndermenize izin vermez. Oda sorguları LiveData döndürdüğünde sorgular, arka planda çalışan bir iş parçacığında otomatik olarak eşzamansız şekilde çalıştırılır.
  • Room, SQLite ifadelerinin derleme zamanı kontrollerini sağlar.

Room veritabanını uygulama

Room veritabanı sınıfınız soyut olmalı ve RoomDatabase sınıfını genişletmelidir. Genellikle, uygulamanın tamamı için yalnızca bir Room veritabanı örneği gerekir.

Şimdi bir tane oluşturalım.

  1. WordRoomDatabase adlı bir Kotlin sınıfı dosyası oluşturun ve bu dosyaya şu kodu ekleyin:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time. 
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java, 
                        "word_database"
                    ).build()
                INSTANCE = instance
                return instance
            }
        }
   }
}

Kodu inceleyelim:

  • Room için veritabanı sınıfı abstract olmalı ve RoomDatabase'ı genişletmelidir.
  • @Database ile sınıfı Room veritabanı olarak açıklama ekler ve veritabanına ait varlıkları bildirmek ve sürüm numarasını ayarlamak için açıklama parametrelerini kullanırsınız. Her varlık, veritabanında oluşturulacak bir tabloya karşılık gelir. Veritabanı taşıma işlemleri bu codelab'in kapsamı dışındadır. Bu nedenle, derleme uyarısını önlemek için burada exportSchema değerini false olarak ayarladık. Gerçek bir uygulamada, şemayı dışa aktarmak için Room'un kullanacağı bir dizin ayarlamayı düşünebilirsiniz. Böylece mevcut şemayı sürüm kontrol sisteminize aktarabilirsiniz.
  • Veritabanı, her @Dao için soyut bir "getter" yöntemi aracılığıyla DAO'ları kullanıma sunar.
  • Aynı anda birden fazla veritabanı örneğinin açılmasını önlemek için tekil (WordRoomDatabase,) tanımladık.
  • getDatabase tek öğeyi döndürür. İlk kez erişildiğinde veritabanını oluşturur. WordRoomDatabase sınıfından uygulama bağlamında bir RoomDatabase nesnesi oluşturmak için Room'un veritabanı oluşturucusunu kullanır ve nesneye "word_database" adını verir.

Depo nedir?

Depo sınıfı, birden fazla veri kaynağına erişimi soyutlar. Depo, Architecture Components kitaplıklarının bir parçası değildir ancak kod ayırma ve mimari için önerilen en iyi uygulamadır. Repository sınıfı, uygulamanın geri kalanına veri erişimi için sorunsuz bir API sağlar.

Neden depo kullanmalısınız?

Depo, sorguları yönetir ve birden fazla arka uç kullanmanıza olanak tanır. En yaygın örnekte, Depo, verilerin ağdan getirilip getirilmeyeceğine veya yerel bir veritabanında önbelleğe alınan sonuçların kullanılıp kullanılmayacağına karar verme mantığını uygular.

Depoyu uygulama

WordRepository adlı bir Kotlin sınıfı dosyası oluşturun ve aşağıdaki kodu bu dosyaya yapıştırın:

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {

    // Room executes all queries on a separate thread.
    // Observed LiveData will notify the observer when the data has changed.
    val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
 
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

Temel çıkarımlar:

  • DAO, veritabanının tamamı yerine depo oluşturucusuna iletilir. Bunun nedeni, veritabanı için tüm okuma/yazma yöntemlerini DAO'nun içermesi ve bu nedenle yalnızca DAO'ya erişimin gerekmesidir. Veritabanının tamamını depoya göstermeniz gerekmez.
  • Kelime listesi kamuya açık bir mülktür. Bu, Room'dan LiveData kelime listesi alınarak başlatılır. "LiveData sınıfı" adımında getAlphabetizedWords yöntemini LiveData döndürecek şekilde tanımladığımız için bunu yapabiliriz. Room, tüm sorguları ayrı bir iş parçacığında yürütür. Ardından, veriler değiştiğinde ana iş parçacığındaki gözlemciye LiveData bildirir.
  • suspend değiştiricisi, derleyiciye bunun bir eşzamanlı rutin veya başka bir askıya alma işlevinden çağrılması gerektiğini bildirir.

ViewModel nedir?

ViewModel'nın görevi, kullanıcı arayüzüne veri sağlamak ve yapılandırma değişikliklerinden etkilenmemektir. ViewModel, Depo ile kullanıcı arayüzü arasında bir iletişim merkezi görevi görür. Parçalar arasında veri paylaşmak için ViewModel da kullanabilirsiniz. ViewModel, lifecycle kitaplığının bir parçasıdır.

Bu konuyla ilgili giriş niteliğinde bir kılavuz için ViewModel Overview veya ViewModels: A Simple Example (ViewModel'ler: Basit Bir Örnek) başlıklı blog yayınını inceleyin.

Neden ViewModel kullanmalısınız?

ViewModel, uygulamanızın kullanıcı arayüzü verilerini yapılandırma değişikliklerinden etkilenmeyecek şekilde yaşam döngüsüne duyarlı olarak tutar. Uygulamanızın kullanıcı arayüzü verilerini Activity ve Fragment sınıflarınızdan ayırmak, tek sorumluluk ilkesini daha iyi uygulamanıza olanak tanır: Etkinlikleriniz ve parçalarınız, verileri ekrana çizmekten sorumludur. ViewModel ise kullanıcı arayüzü için gereken tüm verileri tutmaktan ve işlemden sorumludur.

ViewModel içinde, kullanıcı arayüzünün kullanacağı veya göstereceği değiştirilebilir veriler için LiveData öğesini kullanın. LiveData kullanmanın çeşitli avantajları vardır:

  • Verilere bir gözlemci yerleştirebilir (değişiklikler için yoklama yapmak yerine) ve yalnızca veriler gerçekten değiştiğinde kullanıcı arayüzünü güncelleyebilirsiniz.
  • Depo ve kullanıcı arayüzü, ViewModel ile tamamen ayrılır.
  • ViewModel'dan (tümü Depo'da işlenir) veritabanı çağrısı yapılmadığı için kod daha kolay test edilebilir.

viewModelScope

Kotlin'de tüm eş yordamlar CoroutineScope içinde çalışır. Bir kapsam, işi aracılığıyla eşzamanlı rutinlerin yaşam süresini kontrol eder. Bir kapsamın işini iptal ettiğinizde, bu kapsamda başlatılan tüm eş yordamlar iptal edilir.

AndroidX lifecycle-viewmodel-ktx kitaplığı, ViewModel sınıfının uzantı işlevi olarak viewModelScope ekler. Bu sayede kapsamlarla çalışabilirsiniz.

ViewModel'de eş yordamlarla çalışma hakkında daha fazla bilgi edinmek için Android Uygulamanızda Kotlin Eş Yordamlarını Kullanma codelab'inin 5. adımına veya Android'de Kolay Eş Yordamlar: viewModelScope blog yayınına göz atın.

ViewModel'i uygulama

WordViewModel için bir Kotlin sınıfı dosyası oluşturun ve bu kodu dosyaya ekleyin:

class WordViewModel(application: Application) : AndroidViewModel(application) {

    private val repository: WordRepository
    // Using LiveData and caching what getAlphabetizedWords returns has several benefits:
    // - We can put an observer on the data (instead of polling for changes) and only update the
    //   the UI when the data actually changes.
    // - Repository is completely separated from the UI through the ViewModel.
    val allWords: LiveData<List<Word>>

    init {
        val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }

    /**
     * Launching a new coroutine to insert the data in a non-blocking way
     */
    fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
        repository.insert(word)
    }
}

Burada:

  • Application parametresini alan ve AndroidViewModel sınıfını genişleten WordViewModel adlı bir sınıf oluşturuldu.
  • Depoya referans tutmak için özel bir üye değişkeni eklendi.
  • Kelime listesini önbelleğe almak için herkese açık bir LiveData üye değişkeni eklendi.
  • init adlı blok, WordRoomDatabase adlı bloktan WordDao adlı bloğa referans alıyor.
  • init bloğunda, WordRoomDatabase temel alınarak WordRepository oluşturuldu.
  • init bloğunda, allWords LiveData'yı kullanarak depoyu başlattı.
  • Deponun insert() yöntemini çağıran bir sarmalayıcı insert() yöntemi oluşturuldu. Bu şekilde, insert() uygulaması kullanıcı arayüzünden kapsüllenir. Ekleme işleminin ana iş parçacığını engellemesini istemediğimiz için yeni bir eş yordam başlatıp depodaki askıya alma işlevi olan ekleme işlevini çağırıyoruz. Belirtildiği gibi, ViewModels'ın yaşam döngüsüne dayalı bir coroutine kapsamı vardır. Bu kapsam, burada kullandığımız viewModelScope olarak adlandırılır.

Ardından, liste ve öğeler için XML düzenini eklemeniz gerekir.

Bu codelab'de, XML'de düzen oluşturma konusunda bilgi sahibi olduğunuz varsayılır. Bu nedenle, yalnızca kodu sağlıyoruz.

AppTheme üst öğesini Theme.MaterialComponents.Light.DarkActionBar olarak ayarlayarak uygulama temanızı materyal yapın. values/styles.xml içindeki liste öğeleri için stil ekleme:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <!-- The default font for RecyclerView items is too small.
    The margin is a simple delimiter between the words. -->
    <style name="word_title">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_marginBottom">8dp</item>
        <item name="android:paddingLeft">8dp</item>
        <item name="android:background">@android:color/holo_orange_light</item>
        <item name="android:textAppearance">@android:style/TextAppearance.Large</item>
    </style>
</resources>

layout/recyclerview_item.xml düzeni ekleme:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textView"
        style="@style/word_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_light" />
</LinearLayout>

layout/activity_main.xml içinde TextView yerine RecyclerView ekleyin ve kayan işlem düğmesi (FAB) ekleyin. Düzeniniz artık şu şekilde görünmelidir:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/recyclerview_item"
        android:padding="@dimen/big_padding"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Kullanılabilen işleme göre Düğme'nin görünümü değişmelidir. Bu nedenle, simgeyi "+" sembolüyle değiştirmek istiyoruz.

Öncelikle yeni bir vektör öğesi eklememiz gerekiyor:

  1. Dosya > Yeni > Vektör Öğesi'ni seçin.
  2. Clip Art: (Klip Sanatı:) alanında Android robot simgesini tıklayın.
  3. "Ekle"yi arayın ve "+" öğesini seçin. Tamam
    'ı tıklayın.
  4. Ardından Sonraki'yi tıklayın.
  5. Simge yolunun main > drawable olduğunu onaylayın ve öğeyi eklemek için Bitir'i tıklayın.
  6. Hâlâ layout/activity_main.xml içindeyseniz FAB'ı yeni çizilebilir öğeyi içerecek şekilde güncelleyin:
<com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"
        android:src="@drawable/ic_add_black_24dp"/>

Verileri RecyclerView içinde göstereceksiniz. Bu, verileri doğrudan TextView içine yerleştirmekten biraz daha iyi bir yöntemdir. Bu Codelab'de RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder ve RecyclerView.Adapter'ün nasıl çalıştığını bildiğiniz varsayılır.

Adaptördeki words değişkeninin verileri önbelleğe aldığını unutmayın. Sonraki görevde, verileri otomatik olarak güncelleyen kodu ekleyeceksiniz.

WordListAdapter için RecyclerView.Adapter'i genişleten bir Kotlin sınıfı dosyası oluşturun. Kod:

class WordListAdapter internal constructor(
        context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var words = emptyList<Word>() // Cached copy of words

    inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        val current = words[position]
        holder.wordItemView.text = current.word
    }

    internal fun setWords(words: List<Word>) {
        this.words = words
        notifyDataSetChanged()
    }

    override fun getItemCount() = words.size
}

RecyclerView yöntemini onCreate() MainActivity olarak ekleyin.

onCreate() yönteminde setContentView:

   val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
   val adapter = WordListAdapter(this)
   recyclerView.adapter = adapter
   recyclerView.layoutManager = LinearLayoutManager(this)

Her şeyin çalıştığından emin olmak için uygulamanızı çalıştırın. Henüz verileri bağlamadığınız için öğe yok.

Veritabanında veri yok. Verileri iki şekilde ekleyebilirsiniz: Veritabanı açıldığında bazı verileri ekleme ve kelime eklemek için Activity ekleme.

Uygulama her başlatıldığında tüm içeriği silmek ve veritabanını yeniden doldurmak için RoomDatabase.Callback oluşturup onOpen()'ı geçersiz kılarsınız. Kullanıcı arayüzü iş parçacığında Room veritabanı işlemleri yapamayacağınız için onOpen(), IO Dispatcher'da bir eşzamanlı rutin başlatır.

Bir eş yordamı başlatmak için CoroutineScope gerekir. WordRoomDatabase sınıfının getDatabase yöntemini, parametre olarak bir coroutine kapsamı da alacak şekilde güncelleyin:

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

WordViewModel öğesinin init bloğundaki veritabanı alma başlatıcıyı, kapsamı da iletecek şekilde güncelleyin:

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

WordRoomDatabase içinde, RoomDatabase.Callback() öğesinin özel bir uygulamasını oluştururuz. Bu uygulama, oluşturucu parametresi olarak CoroutineScope alır. Ardından, veritabanını doldurmak için onOpen yöntemini geçersiz kılarız.

Geri çağırma işlevini WordRoomDatabase sınıfı içinde oluşturmaya yönelik kod aşağıda verilmiştir:

private class WordDatabaseCallback(
    private val scope: CoroutineScope
) : RoomDatabase.Callback() {

    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
        INSTANCE?.let { database ->
            scope.launch {
                populateDatabase(database.wordDao())
            }
        }
    }

    suspend fun populateDatabase(wordDao: WordDao) {
        // Delete all content here.
        wordDao.deleteAll()

        // Add sample words.
        var word = Word("Hello")
        wordDao.insert(word)
        word = Word("World!")
        wordDao.insert(word)

        // TODO: Add your own words!
    }
}

Son olarak, Room.databaseBuilder() üzerinde .build() çağrılmadan hemen önce geri aramayı veritabanı oluşturma sırasına ekleyin:

.addCallback(WordDatabaseCallback(scope))

Nihai kod şöyle görünmelidir:

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {

       override fun onOpen(db: SupportSQLiteDatabase) {
           super.onOpen(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()

                   // Delete all content here.
                   wordDao.deleteAll()

                   // Add sample words.
                   var word = Word("Hello")
                   wordDao.insert(word)
                   word = Word("World!")
                   wordDao.insert(word)

                   // TODO: Add your own words!
                   word = Word("TODO!")
                   wordDao.insert(word)
               }
           }
       }
   }

   companion object {
       @Volatile
       private var INSTANCE: WordRoomDatabase? = null

       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                 .addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                // return instance
                instance
        }
     }
   }
}

values/strings.xml içine aşağıdaki dize kaynaklarını ekleyin:

<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>

Bu renk kaynağını value/colors.xml içine ekleyin:

<color name="buttonLabel">#FFFFFF</color>

Yeni bir boyut kaynak dosyası oluşturun:

  1. Project (Proje) penceresinde uygulama modülünü tıklayın.
  2. Dosya > Yeni > Android Kaynak Dosyası'nı seçin.
  3. Kullanılabilir niteleyiciler arasından Boyut 'u seçin.
  4. Dosya adını ayarlayın: dimens

values/dimens.xml'ya aşağıdaki boyut kaynaklarını ekleyin:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>

Boş Etkinlik şablonunu kullanarak yeni bir boş Android Activity oluşturun:

  1. Dosya > Yeni > Etkinlik > Boş Etkinlik'i seçin.
  2. Etkinlik adı için NewWordActivity girin.
  3. Yeni etkinliğin Android Manifest'e eklendiğini doğrulayın.
<activity android:name=".NewWordActivity"></activity>

Düzen klasöründeki activity_new_word.xml dosyasını aşağıdaki kodla güncelleyin:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/edit_word"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="@dimen/min_height"
        android:fontFamily="sans-serif-light"
        android:hint="@string/hint_word"
        android:inputType="textAutoComplete"
        android:layout_margin="@dimen/big_padding"
        android:textSize="18sp" />

    <Button
        android:id="@+id/button_save"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="@string/button_save"
        android:layout_margin="@dimen/big_padding"
        android:textColor="@color/buttonLabel" />

</LinearLayout>

Etkinliğin kodunu güncelleyin:

class NewWordActivity : AppCompatActivity() {

    private lateinit var editWordView: EditText

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        editWordView = findViewById(R.id.edit_word)

        val button = findViewById<Button>(R.id.button_save)
        button.setOnClickListener {
            val replyIntent = Intent()
            if (TextUtils.isEmpty(editWordView.text)) {
                setResult(Activity.RESULT_CANCELED, replyIntent)
            } else {
                val word = editWordView.text.toString()
                replyIntent.putExtra(EXTRA_REPLY, word)
                setResult(Activity.RESULT_OK, replyIntent)
            }
            finish()
        }
    }

    companion object {
        const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
    }
}

Son adım, kullanıcının girdiği yeni kelimeleri kaydederek ve kelime veritabanının mevcut içeriğini RecyclerView içinde göstererek kullanıcı arayüzünü veritabanına bağlamaktır.

Veritabanının mevcut içeriğini görüntülemek için LiveData öğesini ViewModel içinde gözlemleyen bir gözlemci ekleyin.

Veriler her değiştiğinde onChanged() geri çağırma işlemi başlatılır. Bu işlem, bağdaştırıcının önbelleğe alınmış verilerini güncellemek ve görüntülenen listeyi yenilemek için bağdaştırıcının setWords() yöntemini çağırır.

MainActivity'da ViewModel için bir üye değişkeni oluşturun:

private lateinit var wordViewModel: WordViewModel

ViewModel cihazınızı Activity mülkünüzle ilişkilendirmek için ViewModelProvider simgesini kullanın.

Activity ilk kez başlatıldığında ViewModelProviders, ViewModel oluşturur. Etkinlik, örneğin yapılandırma değişikliği yoluyla yok edildiğinde ViewModel kalıcı olur. Etkinlik yeniden oluşturulduğunda ViewModelProviders mevcut ViewModel döndürülür. Daha fazla bilgi için ViewModel başlıklı makaleyi inceleyin.

onCreate() bölümündeki RecyclerView kod bloğunun altında, ViewModelProvider bölümünden bir ViewModel alın:

wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

Ayrıca onCreate() içinde, WordViewModel.
öğesinden allWords LiveData özelliği için bir gözlemci ekleyin.

onChanged() yöntemi (Lambda'mızın varsayılan yöntemi), gözlemlenen veriler değiştiğinde ve etkinlik ön planda olduğunda tetiklenir:

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

FAB'a dokunulduğunda NewWordActivity'yı açmak ve MainActivity'a geri döndüğümüzde yeni kelimeyi veritabanına eklemek veya Toast göstermek istiyoruz. Bunu yapmak için öncelikle bir istek kodu tanımlayarak başlayalım:

private val newWordActivityRequestCode = 1

MainActivity bölümünde NewWordActivity için onActivityResult() kodunu ekleyin.

Etkinlik RESULT_OK ile döndürülürse WordViewModel öğesinin insert() yöntemini çağırarak döndürülen kelimeyi veritabanına ekleyin:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
        data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
            val word = Word(it)
            wordViewModel.insert(word)
        }
    } else {
        Toast.makeText(
            applicationContext,
            R.string.empty_not_saved,
            Toast.LENGTH_LONG).show()
    }
}

Kullanıcı, FAB'ye dokunduğunda MainActivity,başlatmaNewWordActivity işleminde. MainActivity onCreate bölümünde, kayan işlem düğmesini bulun ve şu kodu kullanarak onClickListener ekleyin:

val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

Tamamlanmış kodunuz aşağıdaki gibi görünmelidir:

class MainActivity : AppCompatActivity() {

   private const val newWordActivityRequestCode = 1
   private lateinit var wordViewModel: WordViewModel

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       setSupportActionBar(toolbar)

       val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
       val adapter = WordListAdapter(this)
       recyclerView.adapter = adapter
       recyclerView.layoutManager = LinearLayoutManager(this)

       wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
       wordViewModel.allWords.observe(this, Observer { words ->
           // Update the cached copy of the words in the adapter.
           words?.let { adapter.setWords(it) }
       })

       val fab = findViewById<FloatingActionButton>(R.id.fab)
       fab.setOnClickListener {
           val intent = Intent(this@MainActivity, NewWordActivity::class.java)
           startActivityForResult(intent, newWordActivityRequestCode)
       }
   }

   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       super.onActivityResult(requestCode, resultCode, data)

       if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
           data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
               val word = Word(it)
               wordViewModel.insert(word)
           }
       } else {
           Toast.makeText(
               applicationContext,
               R.string.empty_not_saved,
               Toast.LENGTH_LONG).show()
       }
   }
}

Şimdi uygulamanızı çalıştırın. NewWordActivity'daki veritabanına bir kelime eklediğinizde kullanıcı arayüzü otomatik olarak güncellenir.

Çalışan bir uygulamanız olduğuna göre şimdi oluşturduklarınızı tekrar edelim. Uygulama yapısını tekrar hatırlatmak isteriz:

Uygulamanın bileşenleri şunlardır:

  • MainActivity: RecyclerView ve WordListAdapter kullanarak listedeki kelimeleri gösterir. MainActivity içinde, veritabanındaki LiveData kelimelerini gözlemleyen ve değiştiğinde bildirilen bir Observer vardır.
  • NewWordActivity: listeye yeni bir kelime eklerse
  • WordViewModel: Veri katmanına erişme yöntemleri sağlar ve MainActivity'nin gözlemci ilişkisini ayarlayabilmesi için LiveData döndürür.*
  • LiveData<List<Word>>: Kullanıcı arayüzü bileşenlerinde otomatik güncellemeleri mümkün kılar. MainActivity içinde, veritabanındaki LiveData kelimelerini gözlemleyen ve değiştiğinde bildirilen bir Observer vardır.
  • Repository: bir veya daha fazla veri kaynağını yönetiyor. Repository, ViewModel'in temel veri sağlayıcıyla etkileşime geçmesi için yöntemler sunar. Bu uygulamada arka uç, Room veritabanıdır.
  • Room: SQLite veritabanı etrafında bir sarmalayıcıdır ve bu veritabanını uygular. Room, daha önce kendinizin yapması gereken birçok işi sizin için yapar.
  • DAO: Yöntem çağrılarını veritabanı sorgularıyla eşler. Böylece, depo getAlphabetizedWords() gibi bir yöntemi çağırdığında Room, SELECT * from word_table ORDER BY word ASC'ı çalıştırabilir.
  • Word: Tek bir kelime içeren öğe sınıfıdır.

* Views ve Activities (ve Fragments) yalnızca ViewModel aracılığıyla verilerle etkileşimde bulunur. Bu nedenle, verilerin nereden geldiği önemli değildir.

Otomatik kullanıcı arayüzü güncellemeleri (reaktif kullanıcı arayüzü) için veri akışı

LiveData kullandığımız için otomatik güncelleme mümkündür. MainActivity içinde, veritabanındaki LiveData kelimelerini gözlemleyen ve değiştiğinde bildirilen bir Observer vardır. Bir değişiklik olduğunda gözlemcinin onChange() yöntemi yürütülür ve WordListAdapter içindeki mWords güncellenir.

Veriler LiveData olduğu için gözlemlenebilir. Gözlemlenen ise WordViewModel allWords özelliği tarafından döndürülen LiveData<List<Word>>'dır.

WordViewModel, arka uçla ilgili her şeyi kullanıcı arayüzü katmanından gizler. Veri katmanına erişim için yöntemler sağlar ve LiveData döndürür. Böylece MainActivity, gözlemci ilişkisini ayarlayabilir. Views ve Activities (ve Fragments) yalnızca ViewModel üzerinden verilerle etkileşimde bulunur. Bu nedenle, verilerin nereden geldiği önemli değildir.

Bu durumda veriler Repository kaynağından gelir. ViewModel, bu Depo'nun neyle etkileşimde bulunduğunu bilmek zorunda değildir. Yalnızca Repository ile nasıl etkileşim kuracağını bilmesi gerekir. Bu da Repository tarafından kullanıma sunulan yöntemlerle yapılır.

Depo, bir veya daha fazla veri kaynağını yönetir. WordListSample uygulamasında bu arka uç, Room veritabanıdır. Room, SQLite veritabanı için bir sarmalayıcıdır ve bu veritabanını uygular. Room, daha önce kendinizin yapması gereken birçok işi sizin için yapar. Örneğin, Room, SQLiteOpenHelper sınıfıyla yaptığınız her şeyi yapar.

DAO, yöntem çağrılarını veritabanı sorgularıyla eşler. Böylece, depo getAllWords() gibi bir yöntemi çağırdığında Room, SELECT * from word_table ORDER BY word ASC sorgusunu çalıştırabilir.

Sorgudan döndürülen sonuç LiveData olarak gözlemlendiğinden, Room'daki veriler her değiştiğinde Observer arayüzünün onChanged() yöntemi yürütülür ve kullanıcı arayüzü güncellenir.

[İsteğe bağlı] Çözüm kodunu indirin

Henüz yapmadıysanız codelab'in çözüm koduna göz atabilirsiniz. GitHub deposuna göz atabilir veya kodu buradan indirebilirsiniz:

Kaynak kodu indirme

İndirilen ZIP dosyasını açın. Bu işlem, uygulamanın tamamını içeren bir kök klasörü (android-room-with-a-view-kotlin) açar.