Android Room dengan View - Kotlin

Tujuan Komponen Arsitektur adalah memberikan panduan tentang arsitektur aplikasi, dengan library untuk tugas umum seperti pengelolaan siklus proses dan persistensi data. Komponen arsitektur 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 (bug kode, kesalahan gramatikal, susunan kata yang tidak jelas, dll.) saat mengerjakan codelab ini, 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 MVP atau MVC. Codelab ini mengimplementasikan arsitektur yang ditentukan dalam Panduan untuk arsitektur aplikasi.

Codelab ini berfokus pada Komponen Arsitektur Android. Konsep dan kode di luar topik disediakan agar Anda dapat dengan mudah menyalin dan menempel.

Jika Anda tidak terbiasa dengan Kotlin, versi codelab ini tersedia dalam bahasa pemrograman Java di sini.

Yang akan Anda lakukan

Dalam codelab ini, Anda akan mempelajari cara mendesain dan membuat konstruksi aplikasi menggunakan Room Komponen Arsitektur, ViewModel, dan LiveData, serta membuat aplikasi yang melakukan hal berikut:

  • Mengimplementasikan arsitektur yang direkomendasikan menggunakan Komponen Arsitektur Android.
  • Menggunakan database untuk mendapatkan dan menyimpan data, serta mengisi otomatis database dengan beberapa kata.
  • Menampilkan semua kata dalam RecyclerView di MainActivity.
  • Membuka aktivitas kedua saat pengguna mengetuk tombol +. Saat pengguna memasukkan kata, tambahkan kata tersebut ke database dan daftar.

Aplikasi ini minimalis tetapi cukup rumit, sehingga dapat digunakan sebagai template untuk membuat aplikasi. Berikut pratinjaunya:

Yang Anda butuhkan

  • Android Studio 3.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.

Ada banyak langkah untuk menggunakan Komponen Arsitektur dan mengimplementasikan arsitektur yang direkomendasikan. Hal terpentingnya adalah membentuk model mental dari apa yang sedang terjadi, memahami bagaimana semua bagian berfungsi bersama, dan bagaimana data mengalir. Saat Anda mengerjakan codelab ini, jangan hanya menyalin dan menempelkan kode, tetapi usahakan untuk membangun pemahaman sendiri.

Untuk memperkenalkan terminologi, berikut adalah pengantar singkat mengenai Komponen Arsitektur dan caranya berfungsi bersama. Perlu diketahui bahwa codelab ini berfokus pada subset komponen, yaitu LiveData, ViewModel, dan Room. Setiap komponen akan dijelaskan lebih lanjut saat Anda menggunakannya.

Diagram ini menunjukkan bentuk dasar arsitektur:

Entity: Class yang dianotasi dan menjelaskan tabel database saat menggunakan Room.

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.

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.

Repositori: Class yang Anda buat dan terutama digunakan untuk mengelola beberapa sumber data.

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.

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.

Ringkasan arsitektur RoomWordSample

Diagram berikut menampilkan semua bagian aplikasi. Setiap kotak di bagian luar (kecuali untuk database SQLite) mewakili class yang akan Anda buat.

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

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 plugin lain 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.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. Tambahkan kode berikut di akhir blok dependencies.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"

// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
  1. Di file build.gradle (Project: RoomWordsSample) Anda, tambahkan nomor versi di akhir file, seperti yang terdapat dalam kode di bawah.
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

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.

  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 harus memberi anotasi pada class tersebut. Anotasi mengidentifikasi bagaimana setiap bagian class ini berkaitan dengan entri dalam database. Room menggunakan informasi untuk membuat 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, yang 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.

Bergantung pada cara data disimpan, hal ini dapat menjadi rumit. Mengamati perubahan data di beberapa komponen aplikasi Anda dapat membuat jalur dependensi yang eksplisit dan kaku antar-komponen. Hal ini antara lain menyebabkan pengujian dan proses debug menjadi sulit.

LiveData, class library siklus proses untuk pengamatan data, menyelesaikan masalah ini. Gunakan nilai hasil dari jenis LiveData dalam deskripsi metode, dan Room akan menghasilkan semua kode yang diperlukan untuk memperbarui LiveData saat database diupdate.

Di WordDao, ubah tanda tangan metode getAlphabetizedWords() sehingga List<Word> yang ditampilkan digabungkan dengan LiveData.

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

Nanti di codelab ini, Anda melacak perubahan data melalui Observer di MainActivity.

Apa yang dimaksud dengan 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 LiveData, 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 {
            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
            }
        }
   }
}

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 kita akan menyetel exportSchema ke salah (false) untuk menghindari peringatan build. Di aplikasi yang sebenarnya, sebaiknya Anda menetapkan 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.
  • Kita telah menentukan singleton, 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.

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

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 LiveData kata dari Room; kita dapat melakukannya karena cara kita menentukan metode getAlphabetizedWords untuk menampilkan LiveData di langkah "Class LiveData". Room menjalankan semua kueri pada thread terpisah. Kemudian, LiveData yang diamati akan memberi tahu observer di thread utama saat data berubah.
  • Pengubah suspend memberi tahu compiler bahwa ini perlu dipanggil dari coroutine atau fungsi penangguhan lain.

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.

Di ViewModel, gunakan LiveData untuk data yang dapat diubah yang akan digunakan atau ditampilkan UI. Menggunakan LiveData memiliki beberapa manfaat:

  • Anda dapat menempatkan observer di data (bukan polling perubahan) dan hanya mengupdate
    UI saat data benar-benar berubah.
  • Repositori dan UI sepenuhnya dipisahkan oleh ViewModel.
  • Tidak ada panggilan database dari ViewModel (semua ini ditangani di Repositori), sehingga kode lebih mudah diuji.

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(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)
    }
}

Di sini kita telah:

  • Membuat class bernama WordViewModel yang mendapatkan Application sebagai parameter dan memperluas AndroidViewModel.
  • Menambahkan variabel anggota pribadi untuk menyimpan referensi ke repositori.
  • Menambahkan variabel anggota LiveData publik untuk menyimpan daftar kata ke cache.
  • Membuat blok init yang mendapatkan referensi ke WordDao dari WordRoomDatabase.
  • Di blok init, membuat WordRepository berdasarkan WordRoomDatabase.
  • Di blok init, lakukan inisialisasi LiveData allWords menggunakan repositori.
  • Membuat metode insert() wrapper yang memanggil metode insert() Repositori. Dengan begitu, implementasi insert() dienkapsulasi dari UI. Kita tidak ingin penyisipan memblokir thread utama, sehingga kita meluncurkan coroutine baru dan memanggil penyisipan repositori, yang merupakan fungsi penangguhan. Seperti yang disebutkan sebelumnya, ViewModel memiliki cakupan coroutine berdasarkan siklus prosesnya yang disebut viewModelScope, yang akan kita gunakan di sini.

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

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 kita akan mengganti ikon dengan simbol '+'.

Pertama, kita perlu menambahkan Vector Asset baru:

  1. Pilih File > New > Vector Asset.
  2. Klik ikon robot Android di kolom Clip Art: .
  3. Telusuri "add" dan pilih aset '+' Klik Oke.
  4. Setelah itu, klik Next.
  5. Konfirmasi jalur ikon sebagai main > drawable dan klik Finish untuk menambahkan aset.
  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.LayoutManager, RecyclerView.ViewHolder, dan RecyclerView.Adapter.

Perlu diperhatikan bahwa variabel words dalam adaptor akan meng-cache data. Pada tugas berikutnya, Anda menambahkan kode yang memperbarui data secara otomatis.

Buat file class Kotlin untuk WordListAdapter yang memperluas RecyclerView.Adapter. Berikut kodenya:

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
}

Tambahkan RecyclerView dalam metode onCreate() dari MainActivity.

Di metode onCreate() setelah setContentView:

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

Jalankan aplikasi Anda untuk memastikan semua berfungsi dengan benar. Tidak ada item, karena Anda belum menghubungkan data.

Tidak ada data dalam database. Anda akan menambahkan data dengan dua cara: Menambahkan beberapa data saat database dibuka, dan menambahkan Activity untuk menambahkan kata.

Untuk menghapus semua konten dan mengisi ulang database setiap kali aplikasi dimulai, buat RoomDatabase.Callback dan ganti onOpen(). Karena Anda tidak dapat melakukan operasi database Room di UI thread, onOpen() akan meluncurkan coroutine pada IO Dispatcher.

Untuk meluncurkan coroutine, kita memerlukan CoroutineScope. Perbarui metode getDatabase dari class WordRoomDatabase, agar mendapatkan cakupan coroutine sebagai parameter juga:

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

Penginisialisasi pengambilan database dalam blok init WordViewModel untuk meneruskan cakupan:

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

Di WordRoomDatabase, kita membuat implementasi kustom RoomDatabase.Callback(), yang juga mendapatkan CoroutineScope sebagai parameter konstruktor. Kemudian, kita 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 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!
    }
}

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

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>

Tambahkan resource warna ini di value/colors.xml:

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

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. Tetapkan nama file: dimens

Tambahkan resource dimensi ini di values/dimens.xml:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</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 variabel anggota untuk ViewModel:

private lateinit var wordViewModel: WordViewModel

Gunakan ViewModelProvider untuk mengaitkan ViewModel dengan Activity Anda.

Saat Activity pertama kali dimulai, ViewModelProviders akan membuat ViewModel. Saat aktivitas dihancurkan, misalnya melalui perubahan konfigurasi, ViewModel akan tetap ada. Saat aktivitas dibuat ulang, ViewModelProviders akan menampilkan ViewModel yang sudah ada. Untuk informasi selengkapnya, lihat ViewModel.

Di onCreate() di bawah blok kode RecyclerView, dapatkan ViewModel dari ViewModelProvider:

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

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.setWords(it) }
})

Kita ingin membuka NewWordActivity saat mengetuk FAB dan, setelah kembali ke MainActivity, untuk memasukkan kata baru ke dalam database atau menampilkan Toast. Untuk melakukannya, mari kita 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 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()
       }
   }
}

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 menggunakan RecyclerView dan WordListAdapter. Di MainActivity, terdapat Observer yang mengamati kata LiveData 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. Di MainActivity, terdapat Observer yang mengamati kata LiveData dari database dan menerima notifikasi jika ada perubahan.
  • 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.
  • Word: adalah class entity yang berisi satu kata.

* Views dan Activities (dan 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 kita 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 diupdate.

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