Android Room dengan View - Kotlin

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:

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 class MainActivity.
  • 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.

8e4b761713e3a76b.png

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.

a70aca8d4b737712.png

  1. Buka Android Studio dan klik Start a new Android Studio project.
  2. Di jendela Create New Project, pilih Empty Activity, lalu klik Next.
  3. Pada layar berikutnya, beri nama aplikasi RoomWordSample, lalu klik Finish.

9b6cbaec81794071.png

Selanjutnya, Anda harus menambahkan library komponen ke file Gradle.

  1. Di Android Studio, klik tab Project dan luaskan folder Gradle Scripts.

Buka build.gradle (Module: app).

  1. Terapkan plugin Kotlin pemroses anotasi kapt dengan menambahkannya setelah bagian plugin ditentukan di bagian atas file build.gradle (Module: app).
apply plugin: 'kotlin-kapt'
  1. Tambahkan blok packagingOptions di dalam blok android untuk mengecualikan modul fungsi atom dari paket dan mencegah peringatan.
  2. Beberapa API yang akan Anda gunakan memerlukan jvmTarget 1.8, jadi tambahkan juga ke blok android.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

}
  1. 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.

  1. 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:

3821ac1a6cb01278.png

Room memungkinkan Anda membuat tabel melalui Entity. Mari kita kerjakan sekarang.

  1. Buat file class Kotlin baru bernama Word yang berisi class data Word. 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.

  1. 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
  1. Membuat file class Kotlin baru bernama WordDao.
  2. 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 menghasilkan List 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.

  1. 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 memperluas RoomDatabase.
  • 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 objek RoomDatabase dalam konteks aplikasi dari class WordRoomDatabase 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.

cdfae5b9b10da57f.png

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 metode getAlphabetizedWords untuk menampilkan Flow 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.

72848dfccfe5777b.png

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 mendapatkan WordRepository sebagai parameter dan memperluas ViewModel. 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 Flow allWords dari Repositori. Anda kemudian mengonversi Flow ke LiveData dengan memanggil asLiveData().
  • membuat metode insert() wrapper yang memanggil metode insert() Repositori. Dengan begitu, implementasi insert() 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 disebut viewModelScope, yang akan Anda gunakan di sini.
  • membuat ViewModel dan mengimplementasikan ViewModelProvider.Factory yang mendapatkan dependensi yang diperlukan sebagai parameter untuk membuat WordViewModel: 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:

  1. Klik modul aplikasi di jendela Project.
  2. Pilih File > New > Android Resource File.
  3. Dari the Available Qualifiers, pilih Dimension.
  4. Beri nama file: dimens

aa5895240838057.png

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:

  1. Pilih File > New > Vector Asset.
  2. Klik ikon robot Android di kolom Clip Art:. 8d935457de8e7a46.png
  3. Telusuri "add" dan pilih aset '+'. Klik OK. 758befc99c8cc794.png
  4. Di jendela Asset Studio, klik Next. 672248bada3cfb25.png
  5. Konfirmasi jalur ikon sebagai main > drawable dan klik Finish untuk menambahkan aset. ef118084f96c6176.png
  6. 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 memperluas ListAdapter.
  • Bagian class DiffUtil.ItemCallback bertingkat dari WordListAdapter.
  • 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 ke TextView. Class yang mengekspos fungsi create() statis yang menangani inflate tata letak.
  • WordsComparator yang menentukan cara komputasi jika dua kata sama atau jika kontennya sama.
  • WordListAdapter akan membuat WordViewHolder di onCreateViewHolder dan mengikatnya di onBindViewHolder.

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.

79cb875d4296afce.png

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:

  1. Pilih File > New > Activity > Empty Activity
  2. Masukkan NewWordActivity untuk nama Activity.
  3. 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:

a70aca8d4b737712.png

Komponen aplikasinya adalah:

  • MainActivity: menampilkan kata dalam daftar menggunakan RecyclerView dan WordListAdapter. Di MainActivity, terdapat Observer 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 dari Flow ke LiveData dengan memanggil flow.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 mengeksekusi SELECT * FROM word_table ORDER BY word ASC**.**
  • DAO dapat mengekspos kueri suspend untuk satu permintaan singkat dan kueri Flow saat Anda ingin mendapatkan notifikasi tentang perubahan dalam database.
  • Word: adalah class entity yang berisi satu kata.
  • Views dan Activities (serta Fragments) hanya berinteraksi dengan data melalui ViewModel. 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:

Download kode sumber

Mengekstrak file zip yang didownload. Ini akan mengekstrak folder root, android-room-with-a-view-kotlin, yang berisi aplikasi yang telah selesai.