Pengantar Room dan Flow

1. Sebelum memulai

Dalam codelab sebelumnya, Anda telah mempelajari dasar-dasar database relasional, serta cara membaca dan menulis data menggunakan perintah SQL: SELECT, INSERT, UPDATE, dan DELETE. Mempelajari cara bekerja menggunakan database relasional adalah keterampilan dasar yang akan Anda gunakan selama pemrograman. Mengetahui cara kerja database relasional juga penting untuk menerapkan persistensi data dalam aplikasi Android, yang akan Anda mulai dalam pelajaran ini.

Cara mudah untuk menggunakan database di aplikasi Android adalah dengan library yang disebut Room. Room adalah library ORM (Object Relational Mapping/Pemetaan Relasional Objek) dan memiliki fungsi yang sesuai dengan namanya, yaitu memetakan tabel dalam database relasional ke objek yang bisa digunakan dalam kode Kotlin. Di pelajaran ini, Anda hanya akan berfokus pada pembacaan data. Dengan menggunakan database yang telah terisi otomatis, Anda akan memuat data dari tabel waktu kedatangan bus dan menampilkannya dalam RecyclerView.

70c597851eba9518.png

Dalam prosesnya, Anda akan mempelajari dasar-dasar penggunaan Room, seperti class database, DAO, entity, dan model tampilan. Anda juga akan diperkenalkan ke class ListAdapter, cara lain untuk menyajikan data dalam RecyclerView, dan flow, fitur bahasa Kotlin yang mirip dengan LiveData yang akan memungkinkan UI Anda merespons perubahan di database.

Prasyarat

  • Pemahaman tentang pemrograman berorientasi objek dan penggunaan class, objek, dan pewarisan di Kotlin.
  • Pengetahuan dasar tentang database relasional dan SQL yang dijelaskan dalam codelab dasar-dasar SQL.
  • Pengalaman menggunakan coroutine Kotlin.

Yang akan Anda pelajari

Di akhir pelajaran ini, Anda seharusnya dapat:

  • Merepresentasikan tabel database sebagai objek Kotlin (entity).
  • Menentukan class database untuk menggunakan Room di aplikasi dan mengisi database secara otomatis dari file.
  • Menentukan class DAO dan menggunakan kueri SQL untuk mengakses database dari kode Kotlin.
  • Menentukan model tampilan agar UI dapat berinteraksi dengan DAO.
  • Cara menggunakan ListAdapter dengan tampilan recycler.
  • Dasar-dasar flow Kotlin dan cara menggunakannya agar UI merespons perubahan pada data pokok.

Yang akan Anda build

  • Membaca data dari database yang telah diisi sebelumnya menggunakan Room dan menampilkannya dalam tampilan recycler di aplikasi jadwal bus sederhana.

2. Memulai

Aplikasi yang akan Anda kerjakan dalam codelab ini disebut Bus Schedule. Aplikasi ini menyajikan daftar halte bus dan waktu kedatangan mulai dari yang paling awal hingga yang paling akhir.

70c597851eba9518.png

Mengetuk baris di layar pertama akan mengarahkan ke layar baru yang hanya menampilkan waktu kedatangan berikutnya dari halte bus yang dipilih.

f477c0942746e584.png

Data halte bus berasal dari database yang sudah dipaket dengan aplikasi. Namun, dalam keadaan saat ini, tidak ada data yang akan ditampilkan saat aplikasi berjalan untuk pertama kalinya. Tugas Anda adalah mengintegrasikan Room sehingga aplikasi menampilkan database waktu kedatangan yang sudah terisi otomatis.

  1. Buka halaman repositori GitHub yang disediakan untuk project.
  2. Pastikan nama cabang cocok dengan nama cabang yang ditentukan dalam codelab. Misalnya, dalam screenshot berikut, nama cabang adalah main (utama).

1e4c0d2c081a8fd2.png

  1. Di halaman GitHub project, klik tombol Code yang akan menampilkan pop-up.

1debcf330fd04c7b.png

  1. Pada pop-up, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
  2. Temukan file di komputer Anda (mungkin di folder Downloads).
  3. Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.

Membuka project di Android Studio

  1. Mulai Android Studio.
  2. Di jendela Welcome to Android Studio, klik Open.

d8e9dbdeafe9038a.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > Open.

8d1fda7396afe8e5.png

  1. Di file browser, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
  2. Klik dua kali pada folder project tersebut.
  3. Tunggu Android Studio membuka project.
  4. Klik tombol Run 8de56cba7583251f.png untuk mem-build dan menjalankan aplikasi. Pastikan aplikasi di-build seperti yang diharapkan.

3. Menambahkan dependensi Room

Seperti library lainnya, Anda harus terlebih dahulu menambahkan dependensi yang diperlukan agar dapat menggunakan Room di aplikasi Bus Schedule. Aplikasi ini hanya memerlukan dua perubahan kecil, satu di setiap file Gradle.

  1. Di file build.gradle tingkat project, tentukan room_version di blok ekstensi.
ext {
   kotlin_version = "1.6.20"
   nav_version = "2.4.1"
   room_version = '2.4.2'
}
  1. Di file build.gradle tingkat aplikasi, di akhir daftar dependensi, tambahkan dependensi berikut.
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
  1. Sinkronkan perubahan dan build project untuk memverifikasi bahwa dependensi telah ditambahkan dengan benar.

Di beberapa halaman berikutnya, Anda akan diperkenalkan dengan komponen yang diperlukan untuk mengintegrasikan Room ke dalam aplikasi: model, DAO, model tampilan, dan class database.

4. Membuat entity

Saat mempelajari database relasional dalam codelab sebelumnya, Anda melihat bagaimana data diatur ke dalam tabel yang terdiri dari beberapa kolom, masing-masing mewakili properti spesifik dari jenis data tertentu. Sama seperti class di Kotlin yang menyediakan template untuk setiap objek, tabel dalam database menyediakan template untuk setiap item atau baris, dalam tabel tersebut. Seharusnya tidak mengherankan jika class Kotlin dapat digunakan untuk mewakili setiap tabel dalam database.

Saat bekerja dengan Room, setiap tabel diwakili oleh class. Di library ORM (Object Relational Mapping/Pemetaan Relasi Objek), seperti Room, ini sering disebut class model, atau entity.

Database untuk aplikasi Bus Schedule hanya terdiri dari satu tabel, yaitu jadwal yang mencakup beberapa informasi dasar tentang kedatangan bus.

  • id: Bilangan bulat yang memberikan ID unik sebagai kunci utama
  • stop_name: String
  • arrival_time: Bilangan bulat

Perhatikan bahwa jenis SQL yang digunakan dalam database sebenarnya adalah INTEGER untuk Int dan TEXT untuk String. Namun, saat menggunakan Room, Anda hanya perlu berfokus pada jenis Kotlin saat menentukan class model. Pemetaan jenis data dalam class model ke class yang digunakan di database ditangani secara otomatis.

Jika project memiliki banyak file, Anda harus mempertimbangkan untuk mengatur file dalam berbagai paket guna memberikan kontrol akses yang lebih baik untuk setiap class dan memudahkan dalam menemukan class terkait. Guna membuat entity untuk tabel "schedule" (jadwal), di paket com.example.busschedule, tambahkan paket baru bernama database. Di paket tersebut, tambahkan paket baru bernama schedule untuk entity Anda. Kemudian di paket database.schedule, buat file baru bernama Schedule.kt dan tentukan class data yang disebut Schedule.

data class Schedule(
)

Seperti yang telah dibahas dalam pelajaran Dasar-Dasar SQL, tabel data harus memiliki kunci utama untuk mengidentifikasi setiap baris secara unik. Properti pertama yang akan Anda tambahkan ke class Schedule adalah bilangan bulat untuk mewakili ID unik. Tambahkan properti baru dan tandai dengan anotasi @PrimaryKey. Tindakan ini akan memberi tahu Room untuk memperlakukan properti ini sebagai kunci utama saat baris baru disisipkan.

@PrimaryKey val id: Int

Tambahkan kolom untuk nama halte bus. Kolom harus berjenis String. Untuk kolom baru, Anda harus menambahkan anotasi @ColumnInfo untuk menentukan nama kolom. Biasanya, nama kolom SQL akan berisi kata-kata yang dipisahkan oleh garis bawah, berbeda dengan lowerCamelCase yang digunakan oleh properti Kotlin. Kami tidak ingin kolom ini bernilai null. Oleh karena itu, Anda harus menandainya dengan anotasi @NonNull.

@NonNull @ColumnInfo(name = "stop_name") val stopName: String,

Waktu kedatangan direpresentasikan dalam database menggunakan bilangan bulat. Ini adalah stempel waktu Unix yang dapat dikonversi menjadi tanggal yang dapat digunakan. Berbagai versi SQL menawarkan cara mengonversi tanggal, tetapi untuk tujuan Anda ini, Anda akan tetap menggunakan fungsi pemformatan tanggal Kotlin. Tambahkan kolom @NonNull berikut ke class model.

@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int

Terakhir, agar Room dapat mengenali class ini sebagai fungsi yang dapat digunakan untuk menentukan tabel database, Anda harus menambahkan anotasi ke class itu sendiri. Tambahkan @Entity pada baris terpisah sebelum nama class.

Secara default, Room menggunakan nama class sebagai nama tabel database. Dengan demikian, nama tabel seperti yang ditentukan oleh class saat ini adalah Schedule. Secara opsional, Anda juga dapat menentukan @Entity(tableName="schedule"), tetapi karena kueri Room tidak peka huruf besar/kecil, Anda dapat menghilangkan nama tabel huruf kecil secara eksplisit di sini.

Class untuk entity jadwal sekarang harus terlihat seperti berikut.

@Entity
data class Schedule(
   @PrimaryKey val id: Int,
   @NonNull @ColumnInfo(name = "stop_name") val stopName: String,
   @NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)

5. Menentukan DAO

Class berikutnya yang harus Anda tambahkan untuk mengintegrasikan Room adalah DAO. DAO adalah singkatan dari Data Access Object dan merupakan class Kotlin yang memberikan akses ke data. Secara khusus, DAO adalah tempat Anda akan menyertakan fungsi untuk membaca dan memanipulasi data. Memanggil fungsi di DAO sama dengan menjalankan perintah SQL di database. Faktanya, fungsi DAO seperti yang akan Anda tentukan dalam aplikasi ini, sering kali menentukan perintah SQL sehingga Anda dapat menentukan dengan tepat apa yang diinginkan dari fungsi tersebut. Pengetahuan Anda tentang SQL dari codelab sebelumnya akan berguna saat menentukan DAO.

  1. Tambahkan class DAO untuk entity Schedule. Dalam paket database.schedule, buat file baru bernama ScheduleDao.kt dan tentukan antarmuka bernama ScheduleDao. Seperti halnya class Schedule, Anda harus menambahkan anotasi, kali ini @Dao, agar antarmuka dapat digunakan dengan Room.
@Dao
interface ScheduleDao {
}
  1. Ada dua layar dalam aplikasi dan masing-masing memerlukan kueri yang berbeda. Layar pertama menampilkan semua halte bus dalam urutan menaik menurut waktu kedatangan. Dalam kasus penggunaan ini, kueri hanya perlu mendapatkan semua kolom dan menyertakan klausul ORDER BY yang sesuai. Kueri ditentukan sebagai string yang diteruskan ke anotasi @Query. Tentukan fungsi getAll() yang menampilkan Daftar objek Schedule termasuk anotasi @Query seperti yang ditunjukkan.
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): List<Schedule>
  1. Untuk kueri kedua, Anda juga ingin memilih semua kolom dari tabel jadwal. Namun, Anda hanya menginginkan hasil yang cocok dengan nama halte yang dipilih sehingga Anda harus menambahkan klausul WHERE. Anda dapat mereferensikan nilai Kotlin dari kueri dengan menambahkan titik dua di awal (:) (misalnya :stopName dari parameter fungsi). Seperti sebelumnya, hasil diurutkan dalam urutan menaik menurut waktu kedatangan. Tentukan fungsi getByStopName() yang menggunakan parameter String yang bernama stopName dan menampilkan List dari objek Schedule dengan anotasi @Query seperti yang ditunjukkan.
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): List<Schedule>

6. Menentukan ViewModel

Setelah menyiapkan DAO, secara teknis Anda memiliki semua yang diperlukan untuk mulai mengakses database dari fragmen. Namun, meskipun berlaku secara umum, hal ini umumnya tidak dianggap sebagai praktik terbaik. Alasannya karena di aplikasi yang lebih kompleks, Anda mungkin memiliki beberapa layar yang hanya mengakses sebagian data tertentu. Meskipun ScheduleDao relatif sederhana, Anda dapat dengan mudah melihat hal ini menjadi lepas kendali saat menggunakan dua atau beberapa layar yang berbeda. Misalnya, DAO mungkin terlihat seperti ini:

@Dao
interface ScheduleDao {

    @Query(...)
    getForScreenOne() ...

    @Query(...)
    getForScreenTwo() ...

    @Query(...)
    getForScreenThree()

}

Meskipun kode untuk Layar 1 dapat mengakses getForScreenOne(), tidak ada alasan kuat untuk mengakses metode lainnya. Sebaliknya, yang dianggap praktik terbaik adalah memisahkan bagian DAO yang Anda tampakkan ke tampilan dalam class terpisah yang disebut model tampilan. Ini adalah pola arsitektur yang umum di aplikasi seluler. Penggunaan model tampilan membantu memastikan pemisahan yang jelas antara kode untuk UI aplikasi Anda dan model datanya. Ini juga membantu pengujian setiap bagian kode Anda secara independen, yaitu topik yang akan dijelajahi lebih lanjut saat melanjutkan perjalanan mengembangkan Android.

ee2524be13171538.png

Dengan menggunakan model tampilan, Anda dapat memanfaatkan class ViewModel. Class ViewModel digunakan untuk menyimpan data terkait UI aplikasi dan juga memperhatikan siklus proses yang artinya class tersebut merespons peristiwa siklus proses seperti halnya aktivitas atau fragmen. Jika peristiwa siklus proses seperti rotasi layar menyebabkan aktivitas atau fragmen dihancurkan dan dibuat ulang, ViewModel terkait tidak perlu dibuat ulang. Hal ini tidak mungkin dilakukan dengan mengakses class DAO secara langsung. Jadi, Anda sebaiknya menggunakan subclass ViewModel untuk memisahkan tanggung jawab pemuatan data dari aktivitas atau fragmen Anda.

  1. Untuk membuat class model tampilan, buat file baru bernama BusScheduleViewModel.kt dalam paket baru yang disebut viewmodels. Tentukan class untuk model tampilan. Class tersebut harus mengambil satu parameter jenis ScheduleDao.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
  1. Anda harus menambahkan metode untuk mendapatkan jadwal lengkap dan jadwal yang difilter berdasarkan nama halte karena model tampilan ini akan digunakan dengan kedua layar. Anda dapat melakukannya dengan memanggil metode yang sesuai dari ScheduleDao.
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()

fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)

Meskipun telah selesai menentukan model tampilan, Anda tidak dapat langsung membuat instance BusScheduleViewModel dan berharap semuanya berfungsi. Class ini harus dipakai oleh objek yang dapat merespons peristiwa siklus proses karena class ViewModel BusScheduleViewModel dimaksudkan untuk mengetahui siklus proses. Jika Anda membuat instance secara langsung di salah satu fragmen, objek fragmen harus menangani semuanya, termasuk semua pengelolaan memori yang berada di luar cakupan yang seharusnya dilakukan kode aplikasi Anda. Sebagai gantinya, Anda dapat membuat class yang disebut factory dan class ini akan membuat instance objek model tampilan untuk Anda.

  1. Untuk membuat factory, di bawah class model tampilan, buat class baru BusScheduleViewModelFactory yang diturunkan dari ViewModelProvider.Factory.
class BusScheduleViewModelFactory(
   private val scheduleDao: ScheduleDao
) : ViewModelProvider.Factory {
}
  1. Anda hanya memerlukan sedikit kode boilerplate untuk membuat instance model tampilan dengan benar. Anda tidak akan melakukan inisialisasi class secara langsung, tetapi mengganti metode bernama create() yang menampilkan BusScheduleViewModelFactory dengan beberapa pemeriksaan error. Implementasikan create() di dalam class BusScheduleViewModelFactory seperti berikut ini.
override fun <T : ViewModel> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) {
           @Suppress("UNCHECKED_CAST")
           return BusScheduleViewModel(scheduleDao) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }

Anda sekarang dapat membuat instance objek BusScheduleViewModelFactory dengan BusScheduleViewModelFactory.create() sehingga model tampilan dapat mengetahui siklus proses tanpa fragmen harus menangani hal ini secara langsung.

7. Membuat class database dan mengisi otomatis database

Setelah menentukan model, DAO, dan model tampilan bagi fragmen untuk mengakses DAO, Anda masih harus memberi tahu tindakan yang harus dilakukan dengan semua class ini kepada Room. Di situlah class AppDatabase akan berperan. Aplikasi Android yang menggunakan Room, seperti milik Anda, membuat subclass dari class RoomDatabase dan memiliki beberapa tanggung jawab utama. Di aplikasi Anda, AppDatabase harus

  1. Menetapkan entity yang ditentukan dalam database.
  2. Memberikan akses ke satu instance dari setiap class DAO.
  3. Melakukan penyiapan tambahan, seperti mengisi database terlebih dahulu.

Mungkin Anda bertanya-tanya mengapa Room tidak bisa menemukan semua entity dan objek DAO. Hal ini mungkin disebabkan oleh aplikasi Anda yang memiliki beberapa database atau sejumlah skenario saat library tidak dapat mengasumsikan maksud Anda sebagai developer. Dengan class AppDatabase, Anda dapat sepenuhnya mengontrol model, class DAO, dan penyiapan database yang ingin Anda lakukan.

  1. Untuk menambahkan class AppDatabase, dalam paket database, buat file baru bernama AppDatabase.kt, dan tentukan class abstrak baru AppDatabase yang diwarisi dari RoomDatabase.
abstract class AppDatabase: RoomDatabase() {
}
  1. Class database memudahkan class lain mengakses class DAO. Tambahkan fungsi abstrak yang menampilkan ScheduleDao.
abstract fun scheduleDao(): ScheduleDao
  1. Saat menggunakan class AppDatabase, Anda ingin memastikan bahwa hanya ada satu instance database untuk mencegah kondisi race atau potensi masalah lainnya. Instance disimpan dalam objek pendamping, dan Anda juga akan memerlukan metode yang menampilkan instance yang ada atau membuat database untuk pertama kalinya. Instance ini ditentukan dalam objek pendamping. Tambahkan companion object berikut tepat di bawah fungsi scheduleDao().
companion object {
}

Di companion object, tambahkan properti bernama INSTANCE dari jenis AppDatabase. Nilai ini awalnya disetel ke null sehingga jenisnya ditandai dengan ?. Nilai ini juga ditandai dengan anotasi @Volatile. Meskipun detail tentang waktu yang tepat untuk menggunakan properti yang tidak stabil sedikit lebih canggih untuk pelajaran ini, Anda dapat menggunakannya untuk instance AppDatabase guna menghindari kemungkinan bug.

@Volatile
private var INSTANCE: AppDatabase? = null

Di bawah properti INSTANCE, tentukan fungsi untuk menampilkan instance AppDatabase:

fun getDatabase(context: Context): AppDatabase {
    return INSTANCE ?: synchronized(this) {
        val instance = Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "app_database")
            .createFromAsset("database/bus_schedule.db")
            .build()
        INSTANCE = instance

        instance
    }
}

Dalam penerapan untuk getDatabase(), Anda menggunakan operator Elvis untuk menampilkan instance database yang ada (jika sudah ada) atau membuat database untuk pertama kalinya jika diperlukan. Di aplikasi ini, karena data sudah diisi sebelumnya. Anda juga memanggil createFromAsset() untuk memuat data yang ada. File bus_schedule.db dapat ditemukan di paket assets.database dalam project Anda.

  1. Sama seperti class model dan DAO, class database memerlukan anotasi yang menyediakan beberapa informasi spesifik. Semua jenis entity (Anda mengakses jenisnya sendiri menggunakan ClassName::class) tercantum dalam array. Database juga diberi nomor versi yang akan Anda tetapkan ke 1. Tambahkan anotasi @Database sebagai berikut.
@Database(entities = arrayOf(Schedule::class), version = 1)

Setelah Anda membuat class AppDatabase, tinggal satu langkah lagi untuk membuatnya dapat digunakan. Anda harus memberikan subclass kustom dari class Application dan membuat properti lazy yang akan menyimpan hasil dari getDatabase().

  1. Di paket com.example.busschedule, tambahkan file baru bernama BusScheduleApplication.kt, dan buat class BusScheduleApplication yang diwarisi dari Application.
class BusScheduleApplication : Application() {
}
  1. Tambahkan properti database jenis AppDatabase. Properti ini harus bersifat lambat dan menampilkan hasil pemanggilan getDatabase() di class AppDatabase Anda.
class BusScheduleApplication : Application() {
   val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
  1. Terakhir, untuk memastikan bahwa class BusScheduleApplication digunakan (bukan class dasar default Application), Anda perlu membuat sedikit perubahan pada manifes. Di AndroidMainifest.xml, tetapkan properti android:name ke com.example.busschedule.BusScheduleApplication.
<application
    android:name="com.example.busschedule.BusScheduleApplication"
    ...

Seperti itulah cara untuk menyiapkan model aplikasi Anda. Anda sudah siap untuk mulai menggunakan data dari Room di UI. Di beberapa halaman berikutnya, Anda akan membuat ListAdapter untuk RecyclerView aplikasi guna menyajikan data jadwal bus dan merespons perubahan data secara dinamis.

8. Membuat ListAdapter

Saatnya menerapkan semua yang telah Anda buat dan menghubungkan model ke tampilan. Sebelumnya, Anda kemungkinan akan menggunakan RecyclerView saat menggunakan RecyclerView.Adapter untuk menampilkan daftar data statis. Meskipun metode ini pasti akan berfungsi untuk aplikasi seperti Bus Schedule, skenario umum saat menggunakan database adalah menangani perubahan pada data secara real time. Meskipun hanya satu konten item yang berubah, seluruh tampilan recycler juga akan di-refresh. Ini tidak akan cukup untuk sebagian besar aplikasi yang menggunakan persistensi.

Alternatif untuk daftar yang berubah secara dinamis disebut ListAdapter. ListAdapter menggunakan AsyncListDiffer untuk menentukan perbedaan antara daftar data lama dan daftar data baru. Kemudian, tampilan recycler hanya diperbarui berdasarkan perbedaan antara kedua daftar tersebut. Hasilnya adalah tampilan recycler Anda lebih berperforma tinggi saat menangani data yang sering diperbarui, seperti yang sering Anda miliki dalam aplikasi database.

f59cc2fd4d72c551.png

Anda hanya perlu membuat satu ListAdapter yang dapat digunakan dengan kedua layar karena UI untuk kedua layar adalah sama.

  1. Buat file baru BusStopAdapter.kt dan class BusStopAdapter seperti yang ditunjukkan. Class ini memperluas ListAdapter generik yang mengambil daftar objek Schedule dan class BusStopViewHolder untuk UI. Untuk BusStopViewHolder, Anda juga meneruskan jenis DiffCallback yang akan segera Anda tentukan. Class BusStopAdapter itu sendiri juga menggunakan parameter, onItemClicked(). Fungsi ini akan digunakan untuk menangani navigasi saat item dipilih di layar pertama, tetapi untuk layar kedua, Anda hanya akan meneruskan fungsi kosong.
class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
}
  1. Serupa dengan adaptor tampilan recycler, Anda memerlukan holder tampilan sehingga dapat mengakses tampilan yang dibuat dari file tata letak dalam kode. Tata letak untuk sel sudah dibuat. Sederhananya, buat class BusStopViewHolder seperti yang ditunjukkan dan terapkan fungsi bind() untuk menyetel teks stopNameTextView ke nama halte dan teks arrivalTimeTextView ke tanggal yang telah diformat.
class BusStopViewHolder(private var binding: BusStopItemBinding): RecyclerView.ViewHolder(binding.root) {
    @SuppressLint("SimpleDateFormat")
    fun bind(schedule: Schedule) {
        binding.stopNameTextView.text = schedule.stopName
        binding.arrivalTimeTextView.text = SimpleDateFormat(
            "h:mm a").format(Date(schedule.arrivalTime.toLong() * 1000)
        )
    }
}
  1. Ganti dan terapkan onCreateViewHolder() serta inflate tata letak dan setel onClickListener() untuk memanggil onItemClicked() item di posisi saat ini.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BusStopViewHolder {
   val viewHolder = BusStopViewHolder(
       BusStopItemBinding.inflate(
           LayoutInflater.from( parent.context),
           parent,
           false
       )
   )
   viewHolder.itemView.setOnClickListener {
       val position = viewHolder.adapterPosition
       onItemClicked(getItem(position))
   }
   return viewHolder
}
  1. Ganti dan terapkan onBindViewHolder() serta untuk mengikat tampilan di posisi yang telah ditentukan.
override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) {
   holder.bind(getItem(position))
}
  1. Ingat class DiffCallback yang Anda tentukan untuk ListAdapter? Ini hanyalah objek yang membantu ListAdapter menentukan item di daftar baru dan lama yang berbeda saat memperbarui daftar. Ada dua metode: areItemsTheSame() memeriksa apakah objeknya (atau baris di database dalam kasus Anda) sama hanya dengan memeriksa ID-nya. areContentsTheSame() akan memeriksa apakah semua properti, bukan hanya ID, tetap sama. Metode ini memungkinkan ListAdapter untuk menentukan item mana yang telah disisipkan, diperbarui, dan dihapus sehingga UI dapat diupdate.

Tambahkan objek pendamping dan terapkan DiffCallback seperti yang ditunjukkan.

companion object {
   private val DiffCallback = object : DiffUtil.ItemCallback<Schedule>() {
       override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
           return oldItem.id == newItem.id
       }

       override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
           return oldItem == newItem
       }
   }
}

Anda hanya perlu menyiapkan adaptor. Anda akan menggunakannya di kedua layar aplikasi.

  1. Pertama, dalam FullScheduleFragment.kt, Anda perlu mendapatkan referensi ke model tampilan.
private val viewModel: BusScheduleViewModel by activityViewModels {
   BusScheduleViewModelFactory(
       (activity?.application as BusScheduleApplication).database.scheduleDao()
   )
}
  1. Selanjutnya, di onViewCreated(), tambahkan kode berikut untuk menyiapkan tampilan recycler dan menetapkan pengelola tata letaknya.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
  1. Kemudian, tetapkan properti adaptor. Tindakan yang diteruskan akan menggunakan stopName untuk membuka layar berikutnya yang dipilih sehingga daftar halte bus dapat difilter.
val busStopAdapter = BusStopAdapter({
   val action = FullScheduleFragmentDirections.actionFullScheduleFragmentToStopScheduleFragment(
       stopName = it.stopName
   )
   view.findNavController().navigate(action)
})
recyclerView.adapter = busStopAdapter
  1. Terakhir, untuk memperbarui tampilan daftar, panggil submitList() dengan meneruskan daftar halte bus dari model tampilan.
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
GlobalScope.launch(Dispatchers.IO) {
   busStopAdapter.submitList(viewModel.fullSchedule())
}
  1. Lakukan hal yang sama di StopScheduleFragment. Pertama, dapatkan referensi ke model tampilan.
private val viewModel: BusScheduleViewModel by activityViewModels {
   BusScheduleViewModelFactory(
       (activity?.application as BusScheduleApplication).database.scheduleDao()
   )
}
  1. Kemudian konfigurasikan tampilan recycler di onViewCreated(). Kali ini Anda hanya perlu meneruskan blok kosong (fungsi) dengan {}. Anda sebenarnya tidak ingin ada yang terjadi saat baris di layar ini diketuk.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
val busStopAdapter = BusStopAdapter({})
recyclerView.adapter = busStopAdapter
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
GlobalScope.launch(Dispatchers.IO) {
   busStopAdapter.submitList(viewModel.scheduleForStopName(stopName))
}
  1. Setelah menyiapkan adaptor, Anda akan mengintegrasikan Room ke aplikasi Bus Schedule. Luangkan waktu sejenak untuk menjalankan aplikasi ini dan Anda akan melihat daftar waktu kedatangan. Mengetuk baris akan membuka layar detail.

9. Merespons perubahan data menggunakan Flow

Meskipun tampilan daftar Anda disiapkan untuk menangani perubahan data secara efisien setiap kali submitList() dipanggil, aplikasi Anda belum dapat menangani update dinamis. Untuk melihatnya sendiri, coba buka Database Inspector dan jalankan kueri berikut untuk menyisipkan item baru ke dalam tabel jadwal.

INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)

Anda akan melihat tidak ada yang terjadi di emulator. Pengguna akan menganggap bahwa data tidak berubah. Anda harus menjalankan ulang aplikasi untuk melihat perubahan.

Masalahnya adalah, List<Schedule> hanya ditampilkan sekali saja dari setiap fungsi DAO. Meskipun data dasar diperbarui, submitList() tidak akan dipanggil untuk mengupdate UI, dan tampilannya tidak akan berubah dari perspektif pengguna.

Untuk memperbaikinya, Anda dapat memanfaatkan fitur Kotlin yang disebut flow asinkron (sering disebut flow) yang akan memungkinkan DAO terus mengeluarkan data dari database. Jika item disisipkan, diperbarui, atau dihapus, hasilnya akan dikirim kembali ke fragmen. Dengan menggunakan fungsi bernama collect(), Anda dapat memanggil submitList() menggunakan nilai baru yang dihasilkan dari flow sehingga ListAdapter dapat mengupdate UI berdasarkan data baru.

  1. Untuk menggunakan flow di Bus Schedule, buka ScheduleDao.kt. Untuk mengonversi fungsi DAO agar menampilkan Flow, cukup ubah jenis nilai yang ditampilkan dari fungsi getAll() menjadi Flow<List<Schedule>>.
fun getAll(): Flow<List<Schedule>>
  1. Demikian juga, perbarui nilai return fungsi getByStopName().
fun getByStopName(stopName: String): Flow<List<Schedule>>
  1. Fungsi dalam model tampilan yang mengakses DAO juga perlu diperbarui. Perbarui nilai return ke Flow<List<Schedule>> untuk fullSchedule() dan scheduleForStopName().
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {

   fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()

   fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
  1. Terakhir, di FullScheduleFragment.kt, busStopAdapter seharusnya diperbarui saat Anda memanggil collect() pada hasil kueri. Fungsi fullSchedule() perlu dipanggil dari coroutine karena merupakan fungsi penangguhan. Ganti barisnya.
busStopAdapter.submitList(viewModel.fullSchedule())

Dengan kode ini yang menggunakan flow yang ditampilkan dari fullSchedule().

lifecycle.coroutineScope.launch {
   viewModel.fullSchedule().collect() {
       busStopAdapter.submitList(it)
   }
}
  1. Lakukan hal yang sama di StopScheduleFragment, namun ganti panggilan ke scheduleForStopName() dengan yang berikut.
lifecycle.coroutineScope.launch {
   viewModel.scheduleForStopName(stopName).collect() {
       busStopAdapter.submitList(it)
   }
}
  1. Setelah melakukan perubahan di atas, Anda dapat menjalankan ulang aplikasi untuk memverifikasi bahwa perubahan data kini ditangani secara real time. Setelah aplikasi berjalan, kembali ke Database Inspector, dan kirim kueri berikut untuk memasukkan waktu kedatangan baru sebelum pukul 08.00.
INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)

Item baru akan muncul di bagian atas daftar.

79d6206fc9911fa9.png

Dasar-dasar aplikasi pembuatan Bus Schedule cukup itu saja. Sejauh ini bagus sekali. Sekarang Anda seharusnya memiliki dasar yang kuat untuk menggunakan Room. Pada jalur berikutnya, Anda akan mendalami Room dengan aplikasi contoh baru dan mempelajari cara menyimpan data yang dibuat pengguna di perangkat.

10. Kode solusi

Kode solusi untuk codelab ini berada dalam project dan modul yang ditampilkan di bawah ini.

  1. Buka halaman repositori GitHub yang disediakan untuk project.
  2. Pastikan nama cabang cocok dengan nama cabang yang ditentukan dalam codelab. Misalnya, dalam screenshot berikut, nama cabang adalah main (utama).

1e4c0d2c081a8fd2.png

  1. Di halaman GitHub project, klik tombol Code yang akan menampilkan pop-up.

1debcf330fd04c7b.png

  1. Pada pop-up, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
  2. Temukan file di komputer Anda (mungkin di folder Downloads).
  3. Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.

Membuka project di Android Studio

  1. Mulai Android Studio.
  2. Di jendela Welcome to Android Studio, klik Open.

d8e9dbdeafe9038a.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > Open.

8d1fda7396afe8e5.png

  1. Di file browser, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
  2. Klik dua kali pada folder project tersebut.
  3. Tunggu Android Studio membuka project.
  4. Klik tombol Run 8de56cba7583251f.png untuk mem-build dan menjalankan aplikasi. Pastikan aplikasi di-build seperti yang diharapkan.

11. Selamat

Rangkuman:

  • Tabel dalam database SQL diwakili di Room dengan class Kotlin yang disebut entity.
  • DAO menyediakan metode yang sesuai dengan perintah SQL yang berinteraksi dengan database.
  • ViewModel adalah komponen siklus proses yang digunakan untuk memisahkan data aplikasi Anda dari tampilan.
  • Class AppDatabase memberi tahu Room entity mana yang akan digunakan, memberikan akses ke DAO, dan melakukan penyiapan saat membuat database.
  • ListAdapter adalah adaptor yang digunakan dengan RecyclerView dan ideal untuk menangani daftar yang diperbarui secara dinamis.
  • Flow adalah fitur Kotlin untuk menampilkan aliran data dan dapat digunakan dengan Room untuk memastikan UI dan database sudah sinkron.

Pelajari lebih lanjut