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:
RecyclerView
ve adaptörler- SQLite veritabanı ve SQLite sorgu dili
- Temel eş yordamlar (Eş yordamlar hakkında bilginiz yoksa Android uygulamanızda Kotlin eş yordamlarını kullanma başlıklı makaleyi inceleyebilirsiniz.)
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 kelimeleriMainActivity
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.
Önerilen mimari bileşenler nelerdir?
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.
- Android Studio'yu açın ve Start a new Android Studio project'i (Yeni bir Android Studio projesi başlat) tıklayın.
- Yeni Proje Oluştur penceresinde Empty Activity'yi (Boş Etkinlik) seçin ve Next'i (İleri) tıklayın.
- 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.
- 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.
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'
- Pakete atomik işlevler modülünün dahil edilmemesi ve uyarıların önlenmesi için
android
bloğunun içinepackagingOptions
bloğunu ekleyin.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- 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"
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.
Word
veri sınıfını içerenWord
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.
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
WordDao
adlı yeni bir Kotlin sınıfı dosyası oluşturun.- 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ıpWords
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.
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ı veRoomDatabase
'ı 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 buradaexportSchema
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 birRoomDatabase
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ındagetAlphabetizedWords
yönteminiLiveData
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özlemciyeLiveData
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 veAndroidViewModel
sınıfını genişletenWordViewModel
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ı bloktanWordDao
adlı bloğa referans alıyor.init
bloğunda,WordRoomDatabase
temel alınarakWordRepository
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ızviewModelScope
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:
- Dosya > Yeni > Vektör Öğesi'ni seçin.
- Clip Art: (Klip Sanatı:) alanında Android robot simgesini tıklayın.
- "Ekle"yi arayın ve "+" öğesini seçin. Tamam
'ı tıklayın. - Ardından Sonraki'yi tıklayın.
- Simge yolunun
main > drawable
olduğunu onaylayın ve öğeyi eklemek için Bitir'i tıklayın. - 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:
- Project (Proje) penceresinde uygulama modülünü tıklayın.
- Dosya > Yeni > Android Kaynak Dosyası'nı seçin.
- Kullanılabilir niteleyiciler arasından Boyut 'u seçin.
- 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:
- Dosya > Yeni > Etkinlik > Boş Etkinlik'i seçin.
- Etkinlik adı için
NewWordActivity
girin. - 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
veWordListAdapter
kullanarak listedeki kelimeleri gösterir.MainActivity
içinde, veritabanındaki LiveData kelimelerini gözlemleyen ve değiştiğinde bildirilen birObserver
vardır.NewWordActivity:
listeye yeni bir kelime eklerseWordViewModel
: 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 birObserver
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:
İ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.