Manzaralı Android Odası - Kotlin

Mimari Bileşenler'in amacı, yaşam döngüsü yönetimi ve veri kalıcılığı gibi sık yapılan görevler için kitaplıklarla uygulama mimarisine rehberlik etmektir. Mimari bileşenler, uygulamanızı daha az ortak kodla sağlam, test edilebilir ve sürdürülebilir bir şekilde yapılandırmanıza yardımcı olur. Mimari Bileşen kitaplıkları, Android Jetpack'in bir parçasıdır.

Bu, codelab'in Kotlin sürümüdür. Java programlama dilindeki sürüme buradan ulaşabilirsiniz.

Bu codelab'den çalışırken herhangi bir sorunla (kod hataları, dil bilgisi hataları, belirsiz 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 odaklı tasarım kavramları ve Android geliştirmenin temelleri hakkında bilgi sahibi olmanız gerekir. Özellikle de:

Ayrıca, MVP veya MVC gibi verileri kullanıcı arayüzünden ayıran yazılım mimarisi modelleri hakkında bilgi edinmeniz de yararlı olur. Bu codelab'de, Uygulama mimarisi kılavuzunda tanımlanan mimari uygulanır.

Bu codelab, Android Mimari Bileşenlerine odaklanmaktadır. Konuları kopyalayıp yapıştırmak için konu dışı kavramlar ve kodlar yeterlidir.

Kotlin hakkında bilginiz yoksa bu codelab'in bir sürümü, burada Java programlama dilinde sunulmaktadır.

Ne yaparsınız?

Bu codelab'de Mimari Bileşenleri Odası, ViewModel ve LiveData kullanarak uygulama tasarlayıp oluşturmayı ve aşağıdakileri yapan bir uygulama oluşturmayı öğreneceksiniz:

  • Android Mimari Bileşenleri'ni kullanarak önerilen mimarimizi uygular.
  • Verileri almak ve kaydetmek için bir veritabanıyla çalışır ve veritabanını bazı kelimelerle önceden doldurur.
  • MainActivity RecyclerView içindeki tüm kelimeleri 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 basittir ancak uygulama geliştirmek için şablon olarak kullanabileceğiniz kadar karmaşıktır. Önizleme:

Gerekenler

  • Android Studio 3.0 veya sonraki sürümler ve bunların nasıl kullanılacağı hakkında bilgi. Android Studio'nun yanı sıra SDK'nızın ve Gradle'ın da güncellendiğinden emin olun.
  • Bir Android cihaz veya emülatör.

Bu codelab'de, uygulamanın tamamını oluşturmak için ihtiyaç duyduğunuz tüm kodlar yer almaktadır.

Mimari Bileşenleri kullanmanın ve önerilen mimarisi uygulamanın birçok adımı vardır. En önemlisi, neler olduğuyla ilgili zihinsel bir model oluşturmak, parçaların nasıl bir araya geldiğini ve verilerin nasıl aktığını anlamaktır. Bu codelab'den yararlanırken, yalnızca kodu kopyalayıp yapıştırmanın yanı sıra kendi iç anlayışınızı oluşturmaya başlayın.

Terimleri tanıtmak için Mimari Bileşenler ve bunların birlikte nasıl çalıştığıyla ilgili kısa bir tanıtım. Bu codelab'in, LiveData, ViewModel ve Room gibi bileşenlerin bir alt kümesine odaklandığını unutmayın. Her bileşen siz kullandıkça daha fazla açıklanır.

Bu şema, mimarinin temel bir biçimini göstermektedir:

Varlık: Oda ile çalışırken veritabanı tablosunu tanımlayan ek açıklamalı sınıf.

SQLite veritabanı: Cihaz depolama alanında. Oda kalıcılığı kitaplığı bu veritabanını sizin için oluşturur ve korur.

DAO: Veri erişimi nesnesi. SQL sorgularının işlevlerle eşlenmesi. DAO kullandığınızda yöntemleri çağırırsınız ve gerisini Oda yapar.

Oda veritabanı: Veritabanı çalışmasını basitleştirir ve temel SQLite veritabanına erişim noktası görevi görür (SQLiteOpenHelper) gizlidir). Oda veritabanı, SQLite veritabanına sorgu göndermek için DAO'yu kullanır.

Veri havuzu: Birden çok veri kaynağını yönetmek için kullanılan bir sınıftır.

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ğı konusunda endişelenmesi gerekmiyor. ViewModel örnekleri Etkinlik/Parça eğlencesinden sonra hayatta kalmaya devam eder.

LiveData: Gözlemlenebilen bir veri sahibi sınıfı. Her zaman verilerin en son sürümünü tutar/önbelleğe alır ve veriler değiştiğinde gözlemleyenleri bilgilendirir. LiveData yaşam döngüsüne duyarlı. Kullanıcı arayüzü bileşenleri yalnızca alakalı verileri gözlemler ve gözlemi durdurmaz. Gözlemle alakalı yaşam döngüsü durumu değişikliklerini bildiğinden LiveData tüm bunları otomatik olarak yönetir.

RoomWordSample mimarisine genel bakış

Aşağıdaki şemada uygulamanın tüm bölümleri gösterilmektedir. Çevreleyen kutuların (SQLite veritabanı hariç) her biri, oluşturacağınız bir sınıfı temsil eder.

  1. Android Studio'yu açın ve Yeni bir Android Studio projesi başlatın'ı tıklayın.
  2. Yeni Proje Oluştur penceresinde, Boş Etkinlik'i seçin ve İleri'yi tıklayın.
  3. Sonraki ekranda, uygulamayı RoomWordSample olarak adlandırın ve Son'u tıklayın.

Daha sonra, Gradle dosyalarınıza bileşen kitaplıklarını eklemeniz gerekir.

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

build.gradle uygulamasını açın (Modül: uygulama).

  1. kapt ek açıklama işlemcisi Kotlin eklentisini, build.gradle (Modül: uygulama) dosyanızın üst kısmında tanımlanan diğer eklentilerden sonra ekleyerek uygulayın.
apply plugin: 'kotlin-kapt'
  1. Paketten atomik işlevler modülünü hariç tutmak ve uyarıları önlemek için android blokunun içine packagingOptions blokunu ekleyin.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. dependencies blokunun sonuna aşağıdaki kodu 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 (Proje: RoomWordsSample) dosyanızda, aşağıdaki kodda belirtildiği gibi sürüm numaralarını dosyanın sonuna ekleyin.
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

Bu uygulamanın verileri yalnızca kelimelerdir ve bu değerleri tutmak için basit bir tabloya ihtiyacınız vardır:

Oda, bir Varlık ile tablo oluşturmanıza olanak tanır. Haydi 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, kelimelerinizle ilgili Varlık'ı (SQLit tablosunu temsil eden) açıklar. Sınıftaki her özellik, tablodaki bir sütunu temsil eder. Oda son olarak, bu özellikleri hem tabloyu oluşturmak hem de nesneleri veritabanındaki satırlardan örnek almak için kullanır.

Kod şu şekildedir:

data class Word(val word: String)

Word sınıfını Oda veritabanı için anlamlı hale getirmek üzere nota eklemeniz gerekir. Ek açıklamalar, bu sınıfın her bölümünün veritabanındaki bir girişle nasıl ilişkili olduğunu tanımlar. Oda, bu bilgileri kod oluşturmak için kullanır.

Ek açıklamaları kendiniz yapıştırırsanız (yapıştırma işlemi yerine) 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 yaptığını görelim:

  • @Entity(tableName = "word_table")
    Her @Entity sınıfı bir SQLite tablosunu temsil eder. Bir varlık olduğunu belirtmek için sınıf açıklamanıza ek açıklama ekleyin. Sınıfın adından farklı olmasını istiyorsanız tablonun adını belirtebilirsiniz. Bu tablo tabloya "kelime_tablo" adını verir.
  • @PrimaryKey
    Her varlık için birincil anahtar gerekir. Basit olması için her kelime kendi birincil anahtarı görevi görür.
  • @ColumnInfo(name = "word")
    Üye değişkeninin adından farklı olmasını istiyorsanız tablodaki sütunun adını belirtir. Sütun “kelime” olarak adlandırılır.
  • Veritabanında depolanan her mülk, Kotlin varsayılanı olan, herkese açık olmalıdır.

Ek açıklamaların tam listesini Oda paketi özet referansında bulabilirsiniz.

DAO nedir?

DAO'da (veri erişimi nesnesi) SQL sorgularını belirtir ve 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 amacıyla DAO'yu kullanır.

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

Varsayılan olarak, tüm sorguların ayrı bir ileti dizisinde çalıştırılması gerekir.

Odanın eş yordam desteği vardır. Bu sayede sorgularınıza suspend değiştiriciyle ek açıklama girebilir ve daha sonra bir eş yordamından veya başka bir askıya alma işlevinden çağrılabilir.

DAO'yu uygulama

Aşağıdakiler için sorgu sağlayan bir DAO yazalım:

  • Tüm kelimeleri alfabetik olarak sıralama
  • Kelime ekleme
  • Tüm kelimeler siliniyor
  1. WordDao adlı yeni bir Kotlin sınıfı dosyası oluşturun.
  2. Aşağıdaki kodu kopyalayıp WordDao içine yapıştırın ve içe aktarma işlemini derlemek için gereken içe aktarma işlemlerini 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 bunları inceleyelim:

  • WordDao bir arayüzdür; DAO'lar arayüzler veya soyut sınıflar olmalıdır.
  • @Dao ek açıklaması, Oda için DAO sınıfı olarak tanımlanır.
  • suspend fun insert(word: Word) : Bir kelime eklemek için askıya alma işlevini tanımlar.
  • @Insert ek açıklaması, SQL sağlamanıza gerek olmayan ö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 bunları bu uygulamada kullanmıyorsunuz.)
  • onConflict = OnConflictStrategy.IGNORE: Seçili OnÇakışan stratejisi, zaten listede bulunanla bire bir aynı olan yeni bir kelimeyi yoksayar. Kullanılabilen çakışma stratejileri hakkında daha fazla bilgi edinmek için dokümanlara göz atın.
  • suspend fun deleteAll(): Tüm kelimeleri silmek için askıya alma işlevi bildirir.
  • Birden çok varlığı silmek için uygun bir ek açıklama olmadığından genel @Query ek açıklamasıyla belirtilir.
  • @Query("DELETE FROM word_table"): @Query ek açıklama için dize parametresi olarak bir SQL sorgusu sağlamanızı gerektirir. Bu da, karmaşık okuma sorgularına ve diğer işlemlere olanak tanır.
  • fun getAlphabetizedWords(): List<Word>: Tüm kelimeleri alma ve List / Words döndürmesini sağlama yöntemi.
  • @Query("SELECT * from word_table ORDER BY word ASC"): Artan düzende sıralanmış kelime listesini döndüren sorgu.

Veriler değiştiğinde genellikle bazı işlemler yapmak istersiniz (ör. güncellenen verileri kullanıcı arayüzünde görüntülemek). Yani, değiştiğinde verilere yanıt verebilmeniz için verileri gözlemlemeniz gerekir.

Verilerin nasıl depolandığına bağlı olarak bu işlem karmaşık 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, diğer testlerin yanı sıra test ve hata ayıklamayı zorlaştırır.

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

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

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

Bu codelab'in ilerleyen bölümlerinde, veri değişikliklerini MainActivity ürününde bir Observer üzerinden takip edeceksiniz.

Oda veritabanı nedir?

  • Oda, SQLite veritabanının üzerinde bir veritabanı katmanıdır.
  • Oda, SQLiteOpenHelper ile çalışırken yaptığınız sıradan görevleri halleder.
  • Room, veritabanına sorgu göndermek için DAO'yu kullanır.
  • Varsayılan olarak, zayıf kullanıcı arayüzü performansından kaçınmak için Oda, ana ileti dizisinde sorgular yayınlamanıza izin vermez. Oda sorguları LiveData döndürdüğünde sorgular otomatik olarak arka plandaki ileti dizisinde çalıştırılır.
  • Oda, SQLite ifadelerinin derleme anında kontrollerini sağlar.

Oda veritabanını uygulama

Oda veritabanı sınıfınız soyut olmalı ve RoomDatabase uzatmalıdır. Uygulamanın tamamı için yalnızca bir Oda veritabanı örneğine ihtiyacınız vardır.

Hemen bir tane oluşturalım.

  1. WordRoomDatabase adında bir Kotlin sınıfı dosyası oluşturun ve bu kodu dosyaya 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
            }
        }
   }
}

Şimdi kodun üzerinden geçelim:

  • Odanın veritabanı sınıfı abstract olmalı ve RoomDatabase süresini uzatmalıdır
  • Sınıfa @Database ile bir Oda veritabanı olarak ek açıklama eklersiniz ve veritabanına ait olan varlıkları belirtmek ve sürüm numarasını ayarlamak için ek 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ı almamak için exportSchema değerini false olarak ayarladık. Gerçek bir uygulamada, şemayı dışa aktarmak üzere Oda için kullanılacak bir dizin oluşturmayı düşünebilirsiniz. Böylece, geçerli şemayı sürüm kontrol sisteminize kontrol edebilirsiniz.
  • Veritabanı, her bir @Dao için soyut "alıcı" yöntemiyle DAO'ları gösterir.
  • Veritabanının aynı anda birden fazla örneğinin açılmasını önlemek için tekli (WordRoomDatabase,) tanımını yaptık.
  • getDatabase, tekili döndürür. WordRoomDatabase veritabanındaki uygulama bağlamında bir RoomDatabase nesnesi oluşturmak ve bunu "word_database" olarak adlandırmak için veritabanına ilk erişildiğinde veritabanını oluşturur.

Veri Deposu nedir?

Kod deposu sınıfı, birden fazla veri kaynağına erişimi soyutlar. Kod deposu, Mimari Bileşenler 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 temiz bir API sağlar.

Neden bir Depo kullanılmalı?

Kod deposu, sorguları yönetir ve birden fazla arka uç kullanmanıza olanak tanır. En yaygın örnekte, Repository bir ağdan veri alma veya yerel veritabanında önbelleğe alınmış sonuçları kullanma kararını verir.

Veri Deposunu Uygulama

WordRepository adında 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şturucuya iletilir. Bunun nedeni, DAO'nun veritabanı için tüm okuma/yazma yöntemlerini içermesi nedeniyle yalnızca DAO'ya erişmesi gerektiğidir. Veritabanının tamamını depoya göstermeniz gerekmez.
  • Kelime listesi, herkese açık bir özelliktir. Bu işlem, Odadan LiveData kelime listesi alarak başlar. Bu işlemi, "LiveData class" adımında LiveData özelliğini döndürmek için getAlphabetizedWords yöntemini nasıl tanımladığımızdan yapabiliriz. Room, tüm sorguları ayrı bir mesaj dizisinde yürütür. Ardından, gözlemlenen LiveData veriler değiştiğinde ana iş parçacığındaki gözlemciyi bilgilendirir.
  • suspend değiştiricisi, derleyiciye bunun bir eş yordamdan veya başka bir askıya alma işlevinden çağrılması gerektiğini bildirir.

ViewModel nedir?

ViewModel adlı iş ortağının rolü, kullanıcı arayüzüne veri sağlamak ve yapılandırma değişikliklerinden sonra hayatta kalmaktır. ViewModel, Kod Deposu ve kullanıcı arayüzü arasında iletişim merkezi görevi görür. Verileri parçalar arasında paylaşmak için de ViewModel kullanabilirsiniz. ViewModel, yaşam döngüsü kitaplığının bir parçasıdır.

Bu konuyla ilgili tanıtım kılavuzu için ViewModel Overview veya ViewModels: Basit Bir Örnek blog yayınına bakın.

Neden ViewModel kullanılmalı?

ViewModel, uygulamanızın kullanıcı arayüzü verilerini, yapılandırma değişikliklerinden sonra hayatta kalmaya devam edecek şekilde tutar. Uygulamanızın kullanıcı arayüzü verilerini Activity ve Fragment sınıflarınızdan ayırmak, tek sorumluluk ilkesini yerine getirmenize olanak sağlar: Ekrana veri çizmek, etkinlikleriniz ve parçalarınızdan sorumludur. ViewModel ise kullanıcı arayüzü için gereken tüm verileri tutma ve işleme yetkisine sahip olabilir.

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

  • Verilere bir gözlemci koyabilir (değişiklikler için anket uygulamak yerine) ve kullanıcı arayüzünü, yalnızca veriler gerçekten değiştiğinde
    güncelleyebilirsiniz.
  • Depo ve kullanıcı arayüzü ViewModel ile tamamen ayrılır.
  • ViewModel üzerinden hiç veritabanı çağrısı yok (bu işlem, depoda gerçekleştirilir ve kodu daha test edilebilir hale getirir.

viewModelScope

Kotlin'de tüm eş yordamlar CoroutineScope içinde çalışır. Kapsam, eş yordamların kullanım ömrünü kontrol eder. Bir kapsam 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 bir viewModelScope ekler ve kapsamlarla çalışmanıza olanak tanır.

ViewModel'de coroutine'ler ile çalışma hakkında daha fazla bilgi edinmek için Android uygulamanızda Kotlin Coroutines'i kullanma codelab'inin 5. adımına veya Android'de kolay coroutine'ler: viewModelScope blog yayınına bakın.

ViewModel'i uygulama

WordViewModel için bir Kotlin sınıfı dosyası oluşturun ve bu kodu ona 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 adlı parametreyi parametre olarak alıp AndroidViewModel süresini uzatan, WordViewModel adında bir sınıf oluşturuldu.
  • Depoya referans içerecek bir gizli üye değişkeni eklendi.
  • Kelime listesini önbelleğe almak için herkese açık bir LiveData üye değişkeni eklendi.
  • WordRoomDatabase öğesinden WordDao öğesine referans veren bir init bloğu oluşturuldu.
  • init bloğunda WordRepository WordRoomDatabase temel alınarak oluşturulmuştur.
  • init blokunda, kod deposu kullanılarak allWords LiveData başlatıldı.
  • Repository's insert() yöntemini çağıran bir sarmalayıcı insert() yöntemi oluşturuldu. Bu şekilde, insert() işlevi kullanıcı arayüzünden kapsüllenir. Ana iş parçacığını engellemek için ekleme yapmak istemiyoruz. Bu nedenle yeni bir eş değer liste başlatıyor ve askıya alma işlevi olan kod deposunu çağırıyoruz. Daha önce de bahsettiğimiz gibi, ModelModeller, yaşam döngülerimize göre burada kullandığımız viewModelScope türünde bir eş yordam kapsamına sahiptir.

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

Bu codelab'de XML'de düzen oluşturmayı bildiğiniz varsayıldığında, yalnızca size kod sağlıyoruz.

AppTheme üst öğesini Theme.MaterialComponents.Light.DarkActionBar olarak ayarlayarak uygulama tema materyalinizi oluşturun. values/styles.xml listesindeki liste öğeleri için bir stil ekleyin:

<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 ekleyin:

<?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 öğesini bir RecyclerView ile değiştirin ve bir kayan işlem düğmesi (FAB) ekleyin. Düzeniniz artık aşağıdaki gibi olacaktır:

<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>

FAB'nızın görünümü, kullanılabilir işleme karşılık gelmelidir. Bu nedenle, simgeyi "'+' sembolüyle değiştirmek istiyoruz.

Öncelikle yeni bir Vector Öğesi eklememiz gerekiyor:

  1. File > New > Vector Item'ı seçin.
  2. Küçük resim: alanındaki Android robot simgesini tıklayın.
  3. &"ekle"yi arayın ve '+' öğesini seçin. Tamam
    'ı tıklayın.
  4. Ardından İleri'yi tıklayın.
  5. Simge yolunu main > drawable olarak onaylayın ve öğeyi eklemek için Son'u tıklayın.
  6. Hâlâ layout/activity_main.xml içinde, FAB'yı yeni çekilebilirliği 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österebilirsiniz. Bu, verileri TextView özelliğine yerleştirmekten biraz daha kolaydır. Bu codelab'de RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder ve RecyclerView.Adapter özelliğinin nasıl çalıştığını bildiğiniz varsayılmaktadır.

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

WordListAdapter için RecyclerView.Adapter süresini kapsayan bir Kotlin sınıfı dosyası oluşturun. Kod şu şekildedir:

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 öğesini MainActivity öğesinin onCreate() yöntemine ekleyin.

setContentView yönteminden sonra onCreate() yönteminde:

   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 veri bağlamadığınız için öğe yok.

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

Uygulama her başlatıldığında tüm içerikleri silmek ve veritabanını yeniden doldurmak için bir RoomDatabase.Callback oluşturursunuz ve onOpen() geçersiz kılınır. Kullanıcı arayüzü ileti dizisinde Oda veritabanı işlemlerini gerçekleştiremeyeceğiniz için onOpen(), IO Dispatcher'da bir eş yordam başlatır.

Coroutine başlatmak için CoroutineScope gerekli. Parametre olarak eş yordam kapsamı da almak için WordRoomDatabase sınıfının getDatabase yöntemini güncelleyin:

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

Kapsamın da başarılı olması için WordViewModel blokunun init bloğundaki veritabanı alma başlatıcıyı güncelleyin:

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

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

WordRoomDatabase sınıfının içinde geri arama oluşturma kodu 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() öğesinde .build() öğesini çağırmadan hemen önce geri çağırmayı veritabanı derleme sırasına ekleyin:

.addCallback(WordDatabaseCallback(scope))

Son kod şu şekilde 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
        }
     }
   }
}

Bu dize kaynaklarını values/strings.xml listesine 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 ürününe ekle:

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

Yeni bir boyut kaynak dosyası oluşturun:

  1. Proje penceresinde uygulama modülünü tıklayın.
  2. Dosya > Yeni > Android Kaynak Dosyası'nı seçin
  3. Kullanılabilir Niteleyicilerden Boyut'u seçin
  4. Dosya adını ayarlama: loş

values/dimens.xml uygulamasında şu boyut kaynaklarını ekleyin:

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

Boş Etkinlik şablonuyla 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ı şu 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>

Etkinlik 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 kaydedip RecyclerView veritabanındaki kelime veritabanının mevcut içeriğini 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 ViewModel içindeki LiveData değerlerini gözlemleyen bir gözlemci ekleyin.

Veri her değiştiğinde, adaptörün önbelleğe alınan verilerini güncellemek ve görüntülenen listeyi yenilemek için adaptörün setWords() yöntemini çağıran onChanged() geri çağırma çağrılır.

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

private lateinit var wordViewModel: WordViewModel

ViewModel cihazınızı Activity ile ilişkilendirmek için ViewModelProvider özelliğini kullanın.

Activity cihazınız ilk kez başlatıldığında ViewModelProviders, ViewModel etiketini oluşturur. Etkinlik kaldırıldığında (örneğin, bir yapılandırma değişikliği aracılığıyla) ViewModel devam eder. Etkinlik yeniden oluşturulduğunda, ViewModelProviders mevcut ViewModel değerini döndürür. Daha fazla bilgi edinmek için ViewModel sayfasına göz atın.

RecyclerView kod bloğunun altındaki onCreate() bölgesinde, ViewModelProvider üzerinden ViewModel kazanın:

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

Ayrıca onCreate() WordViewModel adlı ürüne ait allWords LiveData özelliği için bir gözlemci ekleyin.

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

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

FAB'a dokunurken NewWordActivity özelliğini açmak ve MainActivity konumuna geri döndüğümüzde yeni kelimeyi veritabanına eklemek veya bir Toast göstermek istiyoruz. Bunun için bir istek kodu tanımlayarak başlayalım:

private val newWordActivityRequestCode = 1

MainActivity içinde, 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()
    }
}

MainActivity,Kullanıcı FAB'ya dokunduğunda NewWordActivity uygulamasını başlatın. MainActivity onCreate bölümünde, FAB'yı bulun ve şu kodla bir onClickListener ekleyin:

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

Bitmiş kodunuz şu şekilde 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 içinde veritabanına bir kelime eklediğinizde kullanıcı arayüzü otomatik olarak güncellenir.

Artık çalışan bir uygulamanız olduğuna göre oluşturduğunuzu özetleyelim. Uygulama yapısı aşağıda tekrar açıklanmıştır:

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 bildirim alan bir Observer vardır.
  • NewWordActivity: listeye yeni bir kelime ekler.
  • WordViewModel: Veri katmanına erişim yöntemleri sunar ve MainActivity'in gözlemci ilişkisini ayarlayabilmesi için LiveData değerini 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ştirildiğinde bilgilendirilen bir Observer bulunur.
  • Repository:, bir veya daha fazla veri kaynağını yönetiyor. Repository, ViewModel'in temel veri sağlayıcısıyla etkileşim kurma yöntemlerini gösterir. Bu uygulamada arka uç bir Oda veritabanıdır.
  • Room: Sarmalayıcıdır ve SQLite veritabanı uygular. Oda sizin için eskiden yaptığınız çok işin bir parçası.
  • DAO: Metod çağrılarını veritabanı sorgularıyla eşler. Böylece, Repository getAlphabetizedWords() gibi bir yöntemi çağırdığında Oda, SELECT * from word_table ORDER BY word ASC yürütebilir.
  • 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 kaynağı önemli değildir.

Otomatik Kullanıcı Arayüzü Güncellemeleri Verisi (Reactive arayüz)

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

Veriler LiveData olduğundan gözlemlenebilir. Gözlemlenen değer, WordViewModel allWords özelliği tarafından döndürülen LiveData<List<Word>>'dir.

WordViewModel, arka uçla ilgili her şeyi kullanıcı arayüzü katmanından gizler. Veri katmanına erişmek için yöntemler sunar ve MainActivity öğesinin gözlemleyici ilişkisini kurabilmesi için LiveData katmanı döndürür. Views ve Activities (ve Fragments) yalnızca ViewModel aracılığıyla verilerle etkileşimde bulunur. Bu nedenle, verilerin kaynağı önemli değildir.

Bu durumda, veriler bir Repository kaynağından gelir. ViewModel, Repository'nin neyle etkileşimde bulunduğunu bilmesine gerek yoktur. Yalnızca, Repository tarafından gösterilen yöntemleri kullanarak Repository ile nasıl etkileşim kuracağını bilmesi gerekir.

Depo bir veya daha fazla veri kaynağını yönetiyor. WordListSample uygulamasında bu arka uç bir Oda veritabanıdır. Oda, bir sarmalayıcıdır ve SQLite veritabanı uygular. Oda sizin için eskiden yaptığınız çok işin bir parçası. Örneğin, Odalar SQLiteOpenHelper sınıfında yaptığınız tüm işlemleri yapar.

DAO, yöntem çağrılarını veritabanı sorgularına eşler. Böylece, Repository getAllWords() gibi bir yöntemi çağırdığında Oda, SELECT * from word_table ORDER BY word ASC yürütebilir.

Sorgudan döndürülen sonuç LiveData olarak gözlemlendiği için Odadaki veriler her değiştiğinde Observer arayüzünün onChanged() yöntemi yürütülüp 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 veri havuzuna bakabilir veya kodu buradan indirebilirsiniz:

Kaynak kodunu indirin

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