Koleksi Komponen Arsitektur Android memberikan panduan tentang arsitektur aplikasi, dengan library untuk tugas umum seperti pengelolaan siklus proses dan persistensi data. Penggunaan komponen arsitektur dapat membantu Anda membuat struktur aplikasi dengan cara yang andal, dapat diuji, dan dapat dikelola dengan lebih sedikit kode boilerplate.
Library Komponen Arsitektur adalah bagian dari Android Jetpack.
Ini adalah versi Kotlin codelab. Versi dalam bahasa pemrograman Java dapat ditemukan di sini.
Jika Anda mengalami masalah apa pun saat mengerjakan codelab ini, seperti bug kode, kesalahan gramatikal, atau sekadar konten yang membingungkan, laporkan masalah tersebut melalui link Laporkan kesalahan di pojok kiri bawah codelab.
Prasyarat
Anda perlu memahami Kotlin, konsep desain berorientasi objek, dan dasar pengembangan Android, khususnya:
RecyclerView
dan adaptor- Database SQLite dan bahasa kueri SQLite
- Coroutine dasar (Jika tidak terbiasa dengan coroutine, Anda dapat memulai dengan Menggunakan Coroutine Kotlin di Aplikasi Android.)
Hal ini juga membantu memahami pola arsitektur software yang memisahkan data dari antarmuka pengguna, seperti Model-View-Presenter (MVP) atau Model-View-Controller (MVC). Codelab ini menerapkan arsitektur yang didefinisikan dalam dokumentasi developer Android Panduan arsitektur aplikasi.
Codelab ini berfokus pada Komponen Arsitektur Android. Konsep dan kode di luar topik disediakan agar Anda dapat dengan mudah menyalin dan menempel.
Yang akan Anda lakukan
Anda akan mempelajari cara mendesain dan membuat konstruksi aplikasi menggunakan komponen arsitektur Room, ViewModel, dan LiveData. Aplikasi Anda akan:
- mengimplementasikan arsitektur yang direkomendasikan menggunakan Komponen Arsitektur Android.
- menggunakan database untuk mendapatkan dan menyimpan data, serta mengisi otomatis database dengan kata-kata sampel.
- menampilkan semua kata dalam
RecyclerView
dalam classMainActivity
. - membuka aktivitas kedua saat pengguna mengetuk tombol +. Saat pengguna memasukkan sebuah kata, kata tersebut akan ditambahkan ke database dan ditampilkan dalam daftar
RecyclerView
.
Aplikasi ini minimalis tetapi cukup rumit, sehingga dapat digunakan sebagai template untuk membuat aplikasi. Berikut pratinjaunya:
Yang akan Anda butuhkan
- Android Studio 4.0 atau yang lebih baru dan pengetahuan tentang cara menggunakannya. Pastikan Android Studio telah diupdate, begitu pun dengan SDK dan Gradle Anda.
- Perangkat Android atau emulator.
Codelab ini menyediakan semua kode yang Anda perlukan untuk membuat aplikasi yang lengkap.
Berikut adalah diagram singkat untuk memperkenalkan Komponen Arsitektur dan cara kerjanya secara bersamaan kepada Anda. Perlu diketahui bahwa codelab ini berfokus pada subset komponen, yaitu LiveData, ViewModel, dan Room. Setiap komponen dijelaskan secara mendetail sebagaimana Anda menggunakannya dalam aplikasi.
LiveData: Class penyimpan data yang dapat diamati. Selalu menahan/menyimpan cache versi terbaru data, dan memberi tahu pengamatnya saat data telah diubah. LiveData
mendukung siklus proses. Komponen UI hanya mengamati data yang relevan dan tidak menghentikan atau melanjutkan pengamatan. LiveData otomatis mengelola semua ini karena mengetahui terjadinya perubahan status siklus proses terkait saat melakukan pengamatan.
ViewModel: Berlaku sebagai pusat komunikasi antara Repositori (data) dan UI. UI tidak perlu lagi menentukan asal data. Instance ViewModel tetap ada saat pembuatan ulang Aktivitas/Fragmen.
Repositori: Class yang Anda buat dan terutama digunakan untuk mengelola beberapa sumber data.
Entity: Class yang dianotasi dan menjelaskan tabel database saat menggunakan Room.
Database Room: Menyederhanakan tugas database dan berfungsi sebagai titik akses ke database SQLite yang mendasarinya (menyembunyikan SQLiteOpenHelper)
. Database Room menggunakan DAO untuk mengeluarkan kueri ke database SQLite.
Database SQLite: Di penyimpanan perangkat. Library persistensi Room membuat dan mengelola database ini untuk Anda.
DAO: Objek akses data. Pemetaan kueri SQL ke fungsi. Saat menggunakan DAO, Anda memanggil metode, dan Room akan menangani sisanya.
Ringkasan arsitektur RoomWordSample
Diagram berikut menunjukkan cara semua bagian aplikasi harus berinteraksi. Setiap kotak persegi panjang (bukan database SQLite) mewakili class yang akan Anda buat.
- Buka Android Studio dan klik Start a new Android Studio project.
- Di jendela Create New Project, pilih Empty Activity, lalu klik Next.
- Pada layar berikutnya, beri nama aplikasi RoomWordSample, lalu klik Finish.
Selanjutnya, Anda harus menambahkan library komponen ke file Gradle.
- Di Android Studio, klik tab Project dan luaskan folder Gradle Scripts.
Buka build.gradle
(Module: app).
- Terapkan plugin Kotlin pemroses anotasi
kapt
dengan menambahkannya setelah bagian plugin ditentukan di bagian atas filebuild.gradle
(Module: app).
apply plugin: 'kotlin-kapt'
- Tambahkan blok
packagingOptions
di dalam blokandroid
untuk mengecualikan modul fungsi atom dari paket dan mencegah peringatan. - Beberapa API yang akan Anda gunakan memerlukan
jvmTarget
1.8, jadi tambahkan juga ke blokandroid
.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
kotlinOptions {
jvmTarget = "1.8"
}
}
- Ganti blok
dependencies
dengan:
dependencies {
implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"
// Dependencies for working with Architecture components
// You'll probably have to update the version numbers in build.gradle (Project)
// Room components
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.lifecycleVersion"
// 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"
// UI
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation "junit:junit:$rootProject.junitVersion"
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
androidTestImplementation ("androidx.test.espresso:espresso-core:$rootProject.espressoVersion", {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestImplementation "androidx.test.ext:junit:$rootProject.androidxJunitVersion"
}
Pada tahap ini, Gradle mungkin melaporkan masalah tentang versi yang tidak ada atau belum ditetapkan. Masalah tersebut harus diperbaiki dengan langkah berikutnya.
- Di file
build.gradle
(Project: RoomWordsSample) Anda, tambahkan nomor versi di akhir file, seperti yang terdapat dalam kode di bawah.
ext {
activityVersion = '1.1.0'
appCompatVersion = '1.2.0'
constraintLayoutVersion = '2.0.2'
coreTestingVersion = '2.1.0'
coroutines = '1.3.9'
lifecycleVersion = '2.2.0'
materialVersion = '1.2.1'
roomVersion = '2.2.5'
// testing
junitVersion = '4.13.1'
espressoVersion = '3.1.0'
androidxJunitVersion = '1.1.2'
}
Data untuk aplikasi ini adalah kata, dan Anda perlu tabel sederhana untuk menyimpan nilai tersebut:
Room memungkinkan Anda membuat tabel melalui Entity. Mari kita kerjakan sekarang.
- Buat file class Kotlin baru bernama
Word
yang berisi class dataWord
. Class ini akan menjelaskan Entity (yang mewakili tabel SQLite) untuk kata Anda. Setiap properti dalam class mewakili kolom dalam tabel. Room pada akhirnya akan menggunakan properti tersebut untuk membuat tabel dan membuat instance objek dari baris dalam database.
Berikut kodenya:
data class Word(val word: String)
Agar class Word
bermakna bagi database Room, Anda perlu membuat atribusi antara class dan database menggunakan anotasi Kotlin. Anda akan menggunakan anotasi khusus untuk mengetahui bagaimana setiap bagian dari class ini berkaitan dengan entri dalam database. Room menggunakan informasi tambahan ini untuk menghasilkan kode.
Jika Anda mengetik anotasi secara manual (bukan menempelkannya), Android Studio akan otomatis mengimpor class anotasi.
- Perbarui class
Word
Anda dengan anotasi seperti yang ditunjukkan dalam kode ini:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Mari kita lihat apa yang dilakukan anotasi tersebut:
@Entity(tableName =
"word_table"
)
Setiap class@Entity
mewakili tabel SQLite. Anotasikan deklarasi class untuk menunjukkan bahwa itu adalah entity. Anda dapat menentukan nama tabel jika ingin namanya berbeda dari nama class. Anotasi ini menamai tabel sebagai "word_table".@PrimaryKey
Setiap entity memerlukan kunci utama. Sederhananya, setiap kata berfungsi sebagai kunci utamanya sendiri.@ColumnInfo(name =
"word"
)
Menentukan nama kolom dalam tabel jika Anda ingin namanya berbeda dari nama variabel anggota. Anotasi ini menamai kolom sebagai "word".- Setiap properti yang disimpan dalam database harus memiliki visibilitas publik, yang merupakan default Kotlin.
Anda dapat menemukan daftar lengkap anotasi di Referensi ringkasan paket Room.
Apa itu DAO?
Di DAO (objek akses data), Anda menentukan kueri SQL dan mengaitkannya dengan panggilan metode. Compiler akan memeriksa SQL dan menghasilkan kueri dari anotasi praktis untuk kueri umum, seperti @Insert
. Room menggunakan DAO untuk membuat API yang bersih untuk kode Anda.
DAO harus berupa antarmuka atau class abstrak.
Secara default, semua kueri harus dijalankan pada thread terpisah.
Room memiliki dukungan coroutine Kotlin. Dukungan ini memungkinkan kueri Anda dianotasi dengan pengubah suspend
, lalu dipanggil dari coroutine atau dari fungsi penangguhan lain.
Mengimplementasikan DAO
Mari kita menulis DAO yang menyediakan kueri untuk:
- Mengurutkan semua kata menurut abjad
- Menyisipkan kata
- Menghapus semua kata
- Membuat file class Kotlin baru bernama
WordDao
. - Menyalin dan menempel kode berikut ke
WordDao
dan memperbaiki impor sesuai kebutuhan untuk mengompilasikannya.
@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()
}
Mari kita pelajari:
WordDao
adalah antarmuka; DAO harus berupa antarmuka atau class abstrak.- Anotasi
@Dao
mengidentifikasikannya sebagai class DAO untuk Room. suspend fun insert(word: Word)
: Mendeklarasikan fungsi penangguhan untuk menyisipkan satu kata.- Anotasi
@Insert
adalah anotasi metode DAO khusus, sehingga Anda tidak perlu menyediakan SQL apa pun. (Ada pula anotasi@Delete
dan@Update
untuk menghapus dan memperbarui baris, tetapi tidak digunakan dalam aplikasi ini.) onConflict = OnConflictStrategy.IGNORE
: Strategi onConflict yang dipilih akan mengabaikan kata baru jika sama persis dengan kata yang sudah ada dalam daftar. Untuk mengetahui lebih lanjut strategi konflik yang tersedia, lihat dokumentasi.suspend fun deleteAll()
: Mendeklarasikan fungsi penangguhan untuk menghapus semua kata.- Tidak ada anotasi praktis untuk menghapus beberapa entity, sehingga tindakan tersebut diberi anotasi dengan
@Query
generik. @Query
("DELETE FROM word_table")
:@Query
mengharuskan Anda menyediakan kueri SQL sebagai parameter string ke anotasi, sehingga memungkinkan kueri baca yang kompleks dan operasi lainnya.fun getAlphabetizedWords(): List<Word>
: Metode untuk mendapatkan semua kata dan menghasilkanList
Words
.@Query(
"SELECT * FROM word_table ORDER BY word ASC"
)
: Kueri yang menghasilkan daftar kata yang diurutkan dalam urutan naik.
Saat data berubah, Anda biasanya ingin melakukan beberapa tindakan, seperti menampilkan data yang diperbarui di UI. Ini berarti Anda harus mengamati data sehingga jika ada perubahan, Anda dapat bertindak.
Untuk mengamati perubahan data, Anda akan menggunakan Flow dari kotlinx-coroutines
. Gunakan nilai hasil dari jenis Flow
dalam deskripsi metode, dan Room akan menghasilkan semua kode yang diperlukan untuk memperbarui Flow
saat database diupdate.
Di WordDao
, ubah tanda tangan metode getAlphabetizedWords()
sehingga List<Word>
yang ditampilkan digabungkan dengan Flow
.
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>
Di bagian berikutnya codelab ini, kita akan mengubah Flow ke LiveData di ViewModel. Tetapi, kita akan membahas komponen ini setelah mengimplementasikannya.
Apa itu database Room**?**
- Room adalah lapisan database di atas database SQLite.
- Room menangani tugas biasa yang Anda gunakan untuk ditangani dengan
SQLiteOpenHelper
. - Room menggunakan DAO untuk mengeluarkan kueri ke database-nya.
- Secara default, untuk menghindari performa UI yang buruk, Room tidak mengizinkan Anda untuk mengeluarkan kueri di thread utama. Saat Kueri Room menghasilkan
Flow
, kueri akan otomatis berjalan secara asinkron di thread latar belakang. - Room menyediakan pemeriksaan waktu kompilasi terhadap pernyataan SQLite.
Mengimplementasikan database Room
Class database Room Anda harus abstrak dan memperluas RoomDatabase
. Biasanya, Anda hanya memerlukan satu instance database Room untuk seluruh aplikasi.
Mari kita buat sekarang.
- Buat file class Kotlin bernama
WordRoomDatabase
dan tambahkan kode ini ke dalamnya:
// 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 {
// 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"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Mari kita pelajari kode tersebut:
- Class database untuk Room harus
abstract
dan memperluasRoomDatabase.
- Anda menganotasi class menjadi database Room dengan
@Database
dan menggunakan parameter anotasi untuk mendeklarasikan entity yang termasuk dalam database dan menetapkan nomor versi. Setiap entity sesuai dengan tabel yang akan dibuat dalam database. Migrasi database tidak termasuk dalam cakupan codelab ini. Jadi di sini,exportSchema
telah disetel ke salah untuk menghindari peringatan build. Di aplikasi yang sebenarnya, coba setel direktori untuk Room yang akan digunakan untuk mengekspor skema agar Anda dapat memeriksa skema saat ini ke dalam sistem kontrol versi. - Database mengekspos DAO melalui metode "getter" abstrak untuk setiap @Dao.
- Anda menentukan singleton, yakni
WordRoomDatabase,
untuk mencegah beberapa instance database dibuka secara bersamaan. getDatabase
akan menghasilkan singleton. Ini akan membuat database saat pertama kali diakses, menggunakan builder database Room untuk membuat objekRoomDatabase
dalam konteks aplikasi dari classWordRoomDatabase
dan menamainya"word_database"
.
Apa itu Repositori?
Class repositori memisahkan akses ke beberapa sumber data. Repositori bukan bagian dari library Komponen Arsitektur, tetapi merupakan praktik terbaik yang disarankan untuk pemisahan kode dan arsitektur. Class Repositori menyediakan API yang bersih untuk akses data ke aplikasi lainnya.
Mengapa menggunakan Repositori?
Repositori mengelola kueri dan memungkinkan Anda menggunakan beberapa backend. Dalam contoh paling umum, Repositori mengimplementasikan logika guna memutuskan apakah mengambil data dari jaringan atau menggunakan hasil yang di-cache di database lokal.
Mengimplementasikan Repositori
Buat file class Kotlin bernama WordRepository
dan tempelkan kode berikut ke dalamnya:
// 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 Flow will notify the observer when the data has changed.
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
// By default Room runs suspend queries off the main thread, therefore, we don't need to
// implement anything else to ensure we're not doing long running database work
// off the main thread.
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
Poin-poin utama:
- DAO diteruskan ke dalam konstruktor repositori, bukan seluruh database. Ini karena repositori hanya memerlukan akses ke DAO, karena DAO berisi semua metode baca/tulis untuk database tersebut. Seluruh database tidak perlu diekspos ke repositori.
- Daftar kata adalah properti publik. Daftar tersebut diinisialisasi dengan mendapatkan daftar
Flow
kata dari Room; Anda dapat melakukannya karena cara Anda menentukan metodegetAlphabetizedWords
untuk menampilkanFlow
di langkah "Mengamati perubahan database". Room menjalankan semua kueri pada thread terpisah. - Pengubah
suspend
memberi tahu compiler bahwa ini perlu dipanggil dari coroutine atau fungsi penangguhan lain. - Room menjalankan kueri penangguhan dari thread utama.
Apa itu ViewModel?
Peran ViewModel
adalah memberikan data ke UI dan mempertahankan perubahan konfigurasi. ViewModel
bertindak sebagai pusat komunikasi antara Repositori dan UI. Anda juga dapat menggunakan ViewModel
untuk berbagi data di antara fragmen. ViewModel adalah bagian dari library siklus proses.
Untuk panduan pengantar topik ini, lihat ViewModel Overview
atau postingan blog ViewModel: Contoh Sederhana.
Mengapa menggunakan ViewModel?
ViewModel
menyimpan data UI aplikasi Anda dengan cara yang sesuai dengan siklus proses agar konfigurasi tidak berubah. Memisahkan data UI aplikasi dari class Activity
dan Fragment
memungkinkan Anda mengikuti prinsip tanggung jawab tunggal dengan lebih baik: Aktivitas dan fragmen Anda bertanggung jawab untuk menarik data ke layar, sedangkan ViewModel
dapat menangani penyimpanan dan pemrosesan semua data yang diperlukan untuk UI.
LiveData dan ViewModel
LiveData adalah penyimpan data yang dapat diamati - Anda dapat menerima notifikasi setiap kali data berubah. Tidak seperti Flow, LiveData mendukung siklus proses, yang berarti akan mengikuti siklus proses komponen lain, seperti Aktivitas dan Fragmen. LiveData akan otomatis menghentikan atau melanjutkan pengamatan bergantung pada siklus proses komponen yang memantau perubahan. Hal ini membuat LiveData menjadi komponen yang tepat untuk digunakan oleh data yang dapat diubah yang akan digunakan atau ditampilkan UI.
ViewModel akan mengubah data dari Repositori, dari Flow ke LiveData, dan mengekspos daftar kata sebagai LiveData ke UI. Hal ini memastikan bahwa setiap kali data berubah dalam database, UI akan otomatis diperbarui.
viewModelScope
Di Kotlin, semua coroutine berjalan di dalam CoroutineScope
. Cakupan mengontrol masa pakai coroutine melalui tugasnya. Saat Anda membatalkan tugas cakupan, tindakan tersebut akan membatalkan semua coroutine yang dimulai dalam cakupan tersebut.
Library lifecycle-viewmodel-ktx
AndroidX menambahkan viewModelScope
sebagai fungsi ekstensi class ViewModel
, yang memungkinkan Anda menangani cakupan.
Untuk mengetahui lebih lanjut cara menggunakan coroutine di ViewModel, lihat Langkah 5 di codelab Menggunakan Coroutine Kotlin di Aplikasi Android atau postingan blog Coroutine Mudah di Android: viewModelScope.
Mengimplementasikan ViewModel
Buat file class Kotlin untuk WordViewModel
dan tambahkan kode ini ke dalamnya:
class WordViewModel(private val repository: WordRepository) : ViewModel() {
// Using LiveData and caching what allWords 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>> = repository.allWords.asLiveData()
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Mari kita uraikan kode ini. Di sini, Anda telah:
- membuat class
WordViewModel
yang mendapatkanWordRepository
sebagai parameter dan memperluasViewModel
. Repositori adalah satu-satunya dependensi yang diperlukan ViewModel. Jika mungkin diperlukan, class lain juga akan diteruskan dalam konstruktor. - menambahkan variabel anggota
LiveData
publik untuk menyimpan daftar kata ke cache. - melakukan inisialisasi
LiveData
dengan FlowallWords
dari Repositori. Anda kemudian mengonversi Flow ke LiveData dengan memanggilasLiveData().
- membuat metode
insert()
wrapper yang memanggil metodeinsert()
Repositori. Dengan begitu, implementasiinsert()
dienkapsulasi dari UI. Kita meluncurkan coroutine baru dan memanggil penyisipan repositori, yang merupakan fungsi penangguhan. Seperti yang disebutkan, ViewModels memiliki cakupan coroutine berdasarkan siklus prosesnya yang disebutviewModelScope
, yang akan Anda gunakan di sini. - membuat ViewModel dan mengimplementasikan
ViewModelProvider.Factory
yang mendapatkan dependensi yang diperlukan sebagai parameter untuk membuatWordViewModel
:WordRepository
.
Dengan menggunakan viewModels
dan ViewModelProvider.Factory
, framework akan menangani siklus proses ViewModel. Ini akan mempertahankan perubahan konfigurasi dan meskipun Aktivitas dibuat ulang, Anda akan selalu mendapatkan instance class WordViewModel
yang tepat.
Selanjutnya, Anda perlu menambahkan tata letak XML untuk daftar dan item.
Codelab ini mengasumsikan bahwa Anda sudah terbiasa membuat tata letak dalam format XML, sehingga kami hanya menyediakan kodenya.
Buat material tema aplikasi Anda dengan menetapkan induk AppTheme
ke Theme.MaterialComponents.Light.DarkActionBar
. Tambahkan gaya untuk item daftar di values/styles.xml
:
<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_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>
Buat file resource dimensi baru:
- Klik modul aplikasi di jendela Project.
- Pilih File > New > Android Resource File.
- Dari the Available Qualifiers, pilih Dimension.
- Beri nama file: dimens
Tambahkan resource dimensi ini di values/dimens.xml
:
<dimen name="big_padding">16dp</dimen>
Tambahkan tata letak layout/recyclerview_item.xml
:
<?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>
Di layout/activity_main.xml
, ganti TextView
dengan RecyclerView
dan tambahkan tombol tindakan mengambang (FAB). Tata letak Anda sekarang akan terlihat seperti ini:
<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>
Tampilan FAB Anda harus sesuai dengan tindakan yang tersedia, jadi Anda akan mengganti ikon dengan simbol '+'.
Pertama, Anda perlu menambahkan Vector Asset baru:
- Pilih File > New > Vector Asset.
- Klik ikon robot Android di kolom Clip Art:.
- Telusuri "add" dan pilih aset '+'. Klik OK.
- Di jendela Asset Studio, klik Next.
- Konfirmasi jalur ikon sebagai
main > drawable
dan klik Finish untuk menambahkan aset. - Masih di
layout/activity_main.xml
, perbarui FAB untuk menyertakan drawable baru:
<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"/>
Anda akan menampilkan data dalam RecyclerView
, yang sedikit lebih baik dibandingkan hanya menampilkan data dalam TextView
. Codelab ini mengasumsikan bahwa Anda mengetahui cara kerja RecyclerView
, RecyclerView.ViewHolder
, dan ListAdapter
.
Anda harus membuat:
- Class
WordListAdapter
yang memperluasListAdapter
. - Bagian class
DiffUtil.ItemCallback
bertingkat dariWordListAdapter.
ViewHolder
yang akan menampilkan setiap kata dalam daftar.
Berikut kodenya:
class WordListAdapter : ListAdapter<Word, WordViewHolder>(WordsComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
return WordViewHolder.create(parent)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = getItem(position)
holder.bind(current.word)
}
class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val wordItemView: TextView = itemView.findViewById(R.id.textView)
fun bind(text: String?) {
wordItemView.text = text
}
companion object {
fun create(parent: ViewGroup): WordViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(view)
}
}
}
class WordsComparator : DiffUtil.ItemCallback<Word>() {
override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem.word == newItem.word
}
}
}
Di sini, Anda memiliki:
- Class
WordViewHolder
, yang memungkinkan kita mengikat teks keTextView
. Class yang mengekspos fungsicreate()
statis yang menangani inflate tata letak. WordsComparator
yang menentukan cara komputasi jika dua kata sama atau jika kontennya sama.WordListAdapter
akan membuatWordViewHolder
dionCreateViewHolder
dan mengikatnya dionBindViewHolder
.
Tambahkan RecyclerView
dalam metode onCreate()
dari MainActivity
.
Di metode onCreate()
setelah setContentView
:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
Jalankan aplikasi Anda untuk memastikan semua berfungsi dengan benar. Tidak ada item, karena Anda belum menghubungkan data.
Anda hanya ingin memiliki satu instance database dan repositori di aplikasi Anda. Cara mudah untuk mencapainya adalah dengan membuatnya sebagai anggota class Application
. Instance ini kemudian hanya akan diambil dari Application kapan pun diperlukan, bukan dikonstruksi setiap saat.
Buat class baru bernama WordsApplication
yang memperluas Application
. Berikut kodenya:
class WordsApplication : Application() {
// Using by lazy so the database and the repository are only created when they're needed
// rather than when the application starts
val database by lazy { WordRoomDatabase.getDatabase(this) }
val repository by lazy { WordRepository(database.wordDao()) }
}
Berikut yang telah Anda lakukan:
- Membuat instance database.
- Membuat instance repositori berdasarkan DAO database.
- Karena objek ini hanya boleh dibuat saat pertama kali diperlukan, bukan saat pengaktifan aplikasi, Anda akan menggunakan delegasi properti Kotlin:
by lazy
.
Setelah Anda membuat class Application, perbarui file AndroidManifest
dan tetapkan WordsApplication
sebagai application
android:name
.
Berikut adalah tampilan tag aplikasi yang benar:
<application
android:name=".WordsApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
Saat ini, tidak ada data di database. Anda akan menambahkan data dengan dua cara: Menambahkan data tertentu saat database dibuat dan menambahkan Activity
untuk menambahkan kata.
Untuk menghapus semua konten dan mengisi ulang database setiap kali aplikasi dibuat, Anda akan membuat RoomDatabase.Callback
dan mengganti onCreate()
. Karena Anda tidak dapat melakukan operasi database Room di UI thread, onCreate()
akan meluncurkan coroutine pada IO Dispatcher.
Untuk meluncurkan coroutine, Anda memerlukan CoroutineScope
. Perbarui metode getDatabase
dari class WordRoomDatabase
untuk juga mendapatkan cakupan coroutine sebagai parameter:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Mengisi database tidak terkait dengan siklus proses UI, karena itu Anda tidak boleh menggunakan CoroutineScope, seperti viewModelScope. Ini terkait dengan siklus proses aplikasi. Anda akan memperbarui WordsApplication
agar berisi applicationScope
, lalu meneruskannya ke WordRoomDatabase.getDatabase
.
class WordsApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob())
// Using by lazy so the database and the repository are only created when they're needed
// rather than when the application starts
val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
val repository by lazy { WordRepository(database.wordDao()) }
}
Di WordRoomDatabase
, Anda akan membuat implementasi kustom RoomDatabase.Callback()
, yang juga mendapatkan CoroutineScope
sebagai parameter konstruktor. Kemudian, Anda mengganti metode onOpen
untuk mengisi database.
Berikut adalah kode untuk membuat callback di dalam class WordRoomDatabase
:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(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!
}
}
Terakhir, tambahkan callback ke urutan build database tepat sebelum memanggil .build()
di Room.databaseBuilder()
:
.addCallback(WordDatabaseCallback(scope))
Tampilan kode akhir akan terlihat seperti berikut:
@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 onCreate(db: SupportSQLiteDatabase) {
super.onCreate(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
}
}
}
}
Tambahkan resource string ini di values/strings.xml
:
<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>
<string name="add_word">Add word</string>
Tambahkan resource warna ini di value/colors.xml
:
<color name="buttonLabel">#FFFFFF</color>
Tambahkan resource dimensi min_height
di values/dimens.xml
:
<dimen name="min_height">48dp</dimen>
Buat Android Activity
baru yang kosong dengan template Empty Activity:
- Pilih File > New > Activity > Empty Activity
- Masukkan
NewWordActivity
untuk nama Activity. - Pastikan bahwa aktivitas baru telah ditambahkan ke Manifes Android.
<activity android:name=".NewWordActivity"></activity>
Perbarui file activity_new_word.xml
di folder tata letak dengan kode berikut:
<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>
Perbarui kode untuk aktivitas ini:
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"
}
}
Langkah terakhir adalah menghubungkan UI ke database dengan menyimpan kata baru yang dimasukkan pengguna dan menampilkan konten database kata saat ini di RecyclerView
.
Untuk menampilkan konten database saat ini, tambahkan pengamat yang mengamati LiveData
di ViewModel
.
Setiap kali data berubah, callback onChanged()
akan dipanggil, yang memanggil metode setWords()
adaptor untuk memperbarui data cache adaptor dan memuat ulang daftar yang ditampilkan.
Di MainActivity
, buat ViewModel
:
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
Untuk membuat ViewModel, Anda menggunakan delegasi viewModels
, dengan meneruskan instance WordViewModelFactory
. Ini dikonstruksi berdasarkan repositori yang diambil dari WordsApplication
.
Selain itu, di onCreate()
, tambahkan pengamat untuk properti allWords LiveData
dari WordViewModel
.
Metode onChanged()
(metode default untuk Lambda kita) aktif saat data yang diamati berubah dan aktivitas berada di latar depan:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.submitList(it) }
})
Anda ingin membuka NewWordActivity
saat mengetuk FAB dan, setelah kembali dalam MainActivity
, ingin memasukkan kata baru ke dalam database atau menampilkan Toast
.
Untuk melakukannya, mulai dengan menentukan kode permintaan:
private val newWordActivityRequestCode = 1
Di MainActivity
, tambahkan kode onActivityResult()
untuk NewWordActivity
.
Jika aktivitas menghasilkan RESULT_OK
, masukkan kata yang dihasilkan ke database dengan memanggil metode insert()
dari WordViewModel
:
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()
}
}
Di MainActivity,
mulai NewWordActivity
saat pengguna mengetuk FAB. Di MainActivity
onCreate
, temukan FAB dan tambahkan onClickListener
dengan kode ini:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
Kode yang sudah selesai akan terlihat seperti ini:
class MainActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
// Add an observer on the LiveData returned by getAlphabetizedWords.
// The onChanged() method fires when the observed data changes and the activity is
// in the foreground.
wordViewModel.allWords.observe(owner = this) { words ->
// Update the cached copy of the words in the adapter.
words.let { adapter.submitList(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, intentData: Intent?) {
super.onActivityResult(requestCode, resultCode, intentData)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
intentData?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let { reply ->
val word = Word(reply)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG
).show()
}
}
}
Sekarang, jalankan aplikasi Anda. Saat Anda menambahkan kata ke database di NewWordActivity
, UI akan otomatis diupdate.
Setelah aplikasi berfungsi dengan benar, mari menyimpulkan yang Anda buat. Berikut adalah struktur aplikasi tadi:
Komponen aplikasinya adalah:
MainActivity
: menampilkan kata dalam daftar menggunakanRecyclerView
danWordListAdapter
. DiMainActivity
, terdapatObserver
yang mengamati kata dari database dan menerima notifikasi jika ada perubahan.NewWordActivity:
menambahkan kata baru ke dalam daftar.WordViewModel
: menyediakan metode untuk mengakses lapisan data, dan menghasilkan LiveData sehingga MainActivity dapat menyiapkan hubungan pengamat.*LiveData<List<Word>>
: Memungkinkan update otomatis di komponen UI. Anda dapat melakukan konversi dariFlow
keLiveData
dengan memanggilflow.toLiveData()
.Repository:
mengelola satu atau beberapa sumber data.Repository
mengekspos metode untuk ViewModel guna berinteraksi dengan penyedia data yang mendasarinya. Di aplikasi ini, backend tersebut adalah database Room.Room
: adalah wrapper di sekitar dan mengimplementasikan database SQLite. Room melakukan banyak tugas untuk Anda yang sebelumnya harus Anda lakukan sendiri.- DAO: memetakan panggilan metode ke kueri database, sehingga saat Repositori memanggil metode seperti
getAlphabetizedWords()
, Room dapat mengeksekusiSELECT * FROM word_table ORDER BY word ASC
**.** - DAO dapat mengekspos kueri
suspend
untuk satu permintaan singkat dan kueriFlow
saat Anda ingin mendapatkan notifikasi tentang perubahan dalam database. Word
: adalah class entity yang berisi satu kata.Views
danActivities
(sertaFragments
) hanya berinteraksi dengan data melaluiViewModel
. Dengan demikian, tidak masalah dari mana data berasal.
Alur Data untuk Update UI Otomatis (UI Reaktif)
Update otomatis memungkinkan karena Anda menggunakan LiveData. Di MainActivity
, terdapat Observer
yang mengamati kata LiveData dari database dan menerima notifikasi jika ada perubahan. Jika ada perubahan, metode onChange()
pengamat akan dijalankan dan memperbarui mWords
di WordListAdapter
.
Data dapat diamati karena berupa LiveData
. Dan yang diamati adalah LiveData<List<Word>>
yang dihasilkan oleh properti WordViewModel
allWords
.
WordViewModel
menyembunyikan semua hal tentang backend dari lapisan UI. Ini memberikan metode untuk mengakses lapisan data, dan menghasilkan LiveData
sehingga MainActivity
dapat menyiapkan hubungan pengamat. Views
dan Activities
(serta Fragments
) hanya berinteraksi dengan data melalui ViewModel
. Dengan demikian, tidak masalah dari mana data berasal.
Dalam hal ini, data berasal dari Repository
. ViewModel
tidak perlu tahu dengan apa Repositori berinteraksi. Tetapi hanya perlu mengetahui cara berinteraksi dengan Repository
, yaitu melalui metode yang diekspos oleh Repository
.
Repositori mengelola satu atau beberapa sumber data. Di aplikasi WordListSample
, backend tersebut adalah database Room. Room adalah wrapper di sekitar dan mengimplementasikan database SQLite. Room melakukan banyak tugas untuk Anda yang sebelumnya harus Anda lakukan sendiri. Misalnya, Room melakukan semua yang biasa Anda lakukan dengan class SQLiteOpenHelper
.
DAO memetakan panggilan metode ke kueri database, sehingga saat Repositori memanggil metode seperti getAllWords()
, Room dapat mengeksekusi SELECT * FROM word_table ORDER BY word ASC
.
Karena hasil yang ditampilkan dari kueri adalah LiveData
yang diamati, setiap kali data di Room berubah, metode onChanged()
antarmuka Observer
akan dijalankan dan UI diperbarui.
[Opsional] Mendownload kode solusi
Anda dapat melihat kode solusi untuk codelab ini, jika belum melakukannya. Anda dapat melihat repositori github atau mendownload kodenya di sini:
Mengekstrak file zip yang didownload. Ini akan mengekstrak folder root, android-room-with-a-view-kotlin
, yang berisi aplikasi yang telah selesai.