1. Sebelum memulai
Codelab ini mengajarkan Anda tentang lapisan data dan cara menyesuaikannya dengan arsitektur aplikasi secara keseluruhan.
Gambar 1. Diagram yang menunjukkan lapisan data sebagai lapisan tempat lapisan UI dan domain bergantung.
Anda akan membangun lapisan data untuk aplikasi pengelolaan tugas. Anda akan membuat sumber data untuk database lokal dan layanan jaringan, serta repositori yang menampilkan, memperbarui, dan menyinkronkan data.
Prasyarat
- Ini adalah codelab perantara, dan Anda harus memiliki pemahaman dasar tentang cara membangun aplikasi Android (lihat di bawah untuk referensi pembelajaran pemula).
- Pengalaman dengan Kotlin, termasuk lambda, Coroutine, dan Flow. Untuk mempelajari cara menulis Kotlin di aplikasi Android, lihat Unit 1 Dasar-dasar Android dalam kursus Kotlin.
- Pemahaman dasar tentang library Hilt (injeksi dependensi) dan Room (penyimpanan database).
- Pengalaman dengan Jetpack Compose. Unit 1 sampai 3 dari Dasar-dasar Android dalam kursus Compose adalah sumber yang bagus untuk belajar tentang Compose.
- Opsional: Baca panduan ringkasan arsitektur dan lapisan data.
- Opsional: Selesaikan codelab Room.
Yang akan Anda pelajari
Dalam codelab ini, Anda akan mempelajari cara:
- Membuat repositori, sumber data, dan model data untuk manajemen data yang efektif dan skalabel.
- Mengekspos data ke lapisan arsitektur lainnya.
- Menangani pembaruan data asinkron dan tugas yang kompleks atau berjalan lama.
- Menyinkronkan data antara beberapa sumber data.
- Membuat pengujian yang memverifikasi perilaku repositori dan sumber data Anda.
Yang akan Anda build
Anda akan membangun aplikasi pengelolaan tugas yang memungkinkan Anda menambahkan tugas dan menandainya sebagai selesai.
Anda tidak akan menulis aplikasi dari awal. Namun, Anda akan mengerjakan aplikasi yang sudah memiliki lapisan UI. Lapisan UI di aplikasi ini berisi layar dan holder status tingkat layar yang diterapkan menggunakan ViewModels.
Selama codelab, Anda akan menambahkan lapisan data, lalu menghubungkannya ke lapisan UI yang sudah ada sehingga aplikasi dapat berfungsi penuh.
Gambar 2. Screenshot layar daftar tugas. | Gambar 3. Screenshot layar detail tugas. |
2. Memulai persiapan
- Mendownload kode:
https://github.com/android/architecture-samples/archive/refs/heads/data-codelab-start.zip
- Atau, Anda dapat membuat clone repositori GitHub untuk kode tersebut:
git clone https://github.com/android/architecture-samples.git git checkout data-codelab-start
- Buka Android Studio dan muat project
architecture-samples
.
Struktur folder
- Buka Project explorer dalam tampilan Android.
Di bawah folder java/com.example.android.architecture.blueprints.todoapp
terdapat beberapa folder.
Gambar 4. Screenshot yang menunjukkan jendela Project explorer Android Studio dalam tampilan Android.
<root>
berisi class tingkat aplikasi, misalnya untuk navigasi, aktivitas utama, dan class aplikasi.addedittask
berisi fitur UI yang memungkinkan pengguna menambah dan mengedit tugas.data
berisi lapisan data. Anda akan lebih banyak bekerja dengan folder ini.di
berisi modul Hilt untuk injeksi dependensi.tasks
berisi fitur UI yang memungkinkan pengguna melihat dan memperbarui daftar tugas.util
berisi class utilitas.
Ada juga dua folder pengujian, yang ditandai dengan teks dalam tanda kurung di akhir nama folder.
androidTest
mengikuti struktur yang sama dengan<root>
tetapi berisi uji instrumentasi.test
mengikuti struktur yang sama dengan<root>
tetapi berisi uji lokal.
Menjalankan project
- Klik ikon putar hijau di toolbar atas.
Gambar 5. Screenshot yang menunjukkan konfigurasi run, perangkat target, dan tombol run Android Studio.
Anda akan melihat layar Daftar Tugas dengan indikator lingkaran berputar pemuatan yang tidak pernah menghilang.
Gambar 6. Screenshot aplikasi dalam keadaan awal dengan indikator lingkaran berputar pemuatan yang tidak pernah menghilang.
Di akhir codelab, daftar tugas akan ditampilkan di layar ini.
Anda dapat melihat kode akhir di codelab dengan memeriksa cabang data-codelab-final
.
git checkout data-codelab-final
Ingatlah untuk menyimpan perubahan Anda terlebih dahulu.
3. Mempelajari lapisan data
Dalam codelab ini, Anda akan mem-build lapisan data untuk aplikasi.
Lapisan data, seperti namanya, adalah lapisan arsitektur yang mengelola data aplikasi Anda. Lapisan data juga berisi logika bisnis—aturan bisnis di dunia nyata yang menentukan cara data aplikasi harus dibuat, disimpan, dan diubah. Pemisahan fokus ini membuat lapisan data dapat digunakan kembali, memungkinkannya ditampilkan di beberapa layar, berbagi informasi di antara berbagai bagian aplikasi, dan mereproduksi logika bisnis di luar UI untuk pengujian unit.
Jenis komponen utama yang membentuk lapisan data adalah model data, sumber data, dan repositori.
Gambar 7. Diagram yang menunjukkan jenis komponen di lapisan data, termasuk dependensi antara model data, sumber data, dan repositori.
Model data
Data aplikasi biasanya direpresentasikan sebagai model data. Model data adalah representasi data dalam memori.
Karena aplikasi ini adalah aplikasi pengelolaan tugas, Anda memerlukan model data untuk tugas. Berikut ini class Task
:
data class Task(
val id: String
val title: String = "",
val description: String = "",
val isCompleted: Boolean = false,
) { ... }
Poin utama tentang model ini adalah bahwa model ini tidak dapat diubah. Lapisan lain tidak dapat mengubah properti tugas. Lapisan tersebut harus menggunakan lapisan data jika ingin membuat perubahan pada tugas.
Model data internal dan eksternal
Task
adalah contoh dari model data eksternal. Model data ini diekspos secara eksternal oleh lapisan data dan dapat diakses oleh lapisan lain. Lain waktu, Anda akan menentukan model data internal yang hanya digunakan di dalam lapisan data.
Menentukan model data untuk setiap representasi model bisnis adalah praktik yang baik. Di aplikasi ini, ada tiga model data.
Nama model | Eksternal atau internal untuk lapisan data? | Merepresentasikan | Sumber data terkait |
| Eksternal | Tugas yang dapat digunakan di mana saja di aplikasi, hanya disimpan di memori atau saat menyimpan status aplikasi | T/A |
| Internal | Tugas yang disimpan dalam database lokal |
|
| Internal | Tugas yang telah diambil dari server jaringan |
|
Sumber data
Sumber data adalah class yang bertanggung jawab untuk membaca dan menulis data ke satu sumber seperti database atau layanan jaringan.
Di aplikasi ini, ada dua sumber data:
TaskDao
adalah sumber data lokal yang membaca dan menulis ke database.NetworkTaskDataSource
adalah sumber data jaringan yang membaca dan menulis ke server jaringan.
Repositori
Repositori harus mengelola model data tunggal. Di aplikasi ini, Anda akan membuat repositori yang mengelola model Task
. Repositori:
- Mengekspos daftar model
Task
. - Menyediakan metode untuk membuat dan memperbarui model
Task
. - Menjalankan logika bisnis, seperti membuat ID unik untuk setiap tugas.
- Menggabungkan atau memetakan model data internal dari sumber data ke dalam model
Task
. - Menyinkronkan sumber data.
Bersiaplah untuk membuat kode.
- Beralih ke tampilan Android dan luaskan paket
com.example.android.architecture.blueprints.todoapp.data
:
Gambar 8. Jendela Project explorer menunjukkan folder dan file.
Class Task
sudah dibuat sehingga bagian lain aplikasi dapat dikompilasi. Mulai sekarang, Anda membuat sebagian besar class lapisan data dari awal dengan menambahkan implementasi ke file .kt
kosong yang disediakan.
4. Menyimpan data secara lokal
Pada langkah ini, Anda akan membuat sumber data dan model data untuk database Room yang menyimpan tugas secara lokal di perangkat.
Gambar 9. Diagram yang menunjukkan hubungan antara repositori tugas, model, sumber data, dan database.
Membuat model data
Untuk menyimpan data di database Room, Anda perlu membuat entity database.
- Buka file
LocalTask.kt
dalamdata/source/local
, lalu tambahkan kode berikut ke dalamnya:
@Entity(
tableName = "task"
)
data class LocalTask(
@PrimaryKey val id: String,
var title: String,
var description: String,
var isCompleted: Boolean,
)
Class LocalTask
mewakili data yang disimpan dalam tabel bernama task
di database Room. Class ini sangat terkait dengan Room dan tidak boleh digunakan untuk sumber data lain seperti DataStore.
Awalan Local
dalam nama class digunakan untuk menunjukkan bahwa data ini disimpan secara lokal. Awalan tersebut juga digunakan untuk membedakan class ini dari model data Task
, yang diekspos ke lapisan lain di aplikasi. Dengan kata lain, LocalTask
bersifat internal untuk lapisan data, dan Task
bersifat eksternal untuk lapisan data.
Membuat sumber data
Sekarang, setelah Anda memiliki model data, buat sumber data untuk membuat, membaca, memperbarui, dan menghapus ( CRUD) model LocalTask
. Karena menggunakan Room, Anda dapat menggunakan Objek Akses Data (anotasi @Dao
) sebagai sumber data lokal.
- Buat antarmuka Kotlin baru di file bernama
TaskDao.kt
.
@Dao
interface TaskDao {
@Query("SELECT * FROM task")
fun observeAll(): Flow<List<LocalTask>>
@Upsert
suspend fun upsert(task: LocalTask)
@Upsert
suspend fun upsertAll(tasks: List<LocalTask>)
@Query("UPDATE task SET isCompleted = :completed WHERE id = :taskId")
suspend fun updateCompleted(taskId: String, completed: Boolean)
@Query("DELETE FROM task")
suspend fun deleteAll()
}
Metode untuk membaca data diawali dengan observe
. Metode ini adalah fungsi non-penangguhan yang menampilkan Flow
. Setiap kali data yang mendasarinya berubah, item baru akan ditampilkan ke aliran data. Fitur yang berguna dari library Room (dan banyak library penyimpanan data lainnya) berarti Anda dapat memproses perubahan data, bukan mengumpulkan database untuk data baru.
Metode untuk menulis data menangguhkan fungsi karena metode tersebut melakukan operasi I/O.
Memperbarui skema database
Hal berikutnya yang perlu Anda lakukan adalah memperbarui database agar dapat menyimpan model LocalTask
.
- Buka
ToDoDatabase.kt
dan ubahBlankEntity
keLocalTask
. - Hapus
BlankEntity
dan pernyataanimport
yang berulang. - Tambahkan metode untuk menampilkan DAO bernama
taskDao
.
Class yang diperbarui akan terlihat seperti ini:
@Database(entities = [LocalTask::class], version = 1, exportSchema = false)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
Memperbarui konfigurasi Hilt
Project ini menggunakan Hilt untuk injeksi dependensi. Hilt perlu mengetahui cara membuat TaskDao
sehingga dapat dimasukkan ke class yang menggunakannya.
- Buka
di/DataModules.kt
dan tambahkan metode berikut keDatabaseModule
:
@Provides
fun provideTaskDao(database: ToDoDatabase) : TaskDao = database.taskDao()
Anda sekarang memiliki semua bagian yang diperlukan untuk membaca dan menulis tugas ke database lokal.
5. Menguji sumber data lokal
Pada langkah terakhir, Anda akan menulis cukup banyak kode, tetapi bagaimana Anda tahu kode itu bekerja dengan benar? Sangat mudah untuk membuat kesalahan dengan semua kueri SQL di TaskDao
. Buat pengujian untuk memverifikasi bahwa TaskDao
berperilaku sebagaimana mestinya.
Pengujian bukan bagian dari aplikasi, sehingga harus ditempatkan di folder yang berbeda. Ada dua folder pengujian yang ditandai dengan teks dalam tanda kurung di akhir nama paket:
Gambar 10. Screenshot yang menunjukkan folder pengujian dan androidTest di Project explorer.
androidTest
berisi pengujian yang dijalankan pada emulator atau perangkat Android. Pengujian ini dikenal sebagai uji instrumentasi.test
berisi pengujian yang dijalankan pada mesin host Anda, juga dikenal sebagai uji lokal.
TaskDao
memerlukan database Room (yang hanya dapat dibuat di perangkat Android). Jadi, untuk mengujinya, Anda perlu membuat uji instrumentasi.
Membuat class pengujian
- Luaskan folder
androidTest
dan bukaTaskDaoTest.kt
. Di dalamnya, buat class kosong bernamaTaskDaoTest
.
class TaskDaoTest {
}
Menambahkan database pengujian
- Tambahkan
ToDoDatabase
dan lakukan inisialisasi sebelum setiap pengujian.
private lateinit var database: ToDoDatabase
@Before
fun initDb() {
database = Room.inMemoryDatabaseBuilder(
getApplicationContext(),
ToDoDatabase::class.java
).allowMainThreadQueries().build()
}
Tindakan ini akan membuat database dalam memori sebelum setiap pengujian. Database dalam memori jauh lebih cepat daripada database berbasis disk. Pilihan ini baik untuk pengujian otomatis dengan data yang tidak perlu bertahan lebih lama dari pengujian.
Menambahkan pengujian
Tambahkan pengujian yang memverifikasi bahwa LocalTask
dapat dimasukkan, dan bahwa LocalTask
yang sama dapat dibaca menggunakan TaskDao
.
Semua pengujian dalam codelab ini mengikuti struktur kondisi, jika, maka:
Kondisi | Database yang kosong |
Jika | Tugas dimasukkan, dan Anda mulai mengamati aliran tugas |
Maka | Item pertama dalam aliran tugas cocok dengan tugas yang dimasukkan |
- Mulailah dengan membuat pengujian yang gagal. Tindakan ini akan memastikan bahwa pengujian benar-benar berjalan dan bahwa objek yang benar serta dependensinya telah diuji.
@Test
fun insertTaskAndGetTasks() = runTest {
val task = LocalTask(
title = "title",
description = "description",
id = "id",
isCompleted = false,
)
database.taskDao().upsert(task)
val tasks = database.taskDao().observeAll().first()
assertEquals(0, tasks.size)
}
- Jalankan pengujian dengan mengklik Play di sebelah pengujian di ruang kosong
Gambar 11. Screenshot yang menunjukkan tombol Play pengujian di ruang kosong editor kode.
Dalam jendela hasil pengujian, Anda akan melihat pengujian gagal dengan pesan expected:<0> but was:<1>
. Hasil ini sudah diduga karena jumlah tugas dalam database adalah satu, bukan nol.
Gambar 12. Screenshot yang menunjukkan pengujian yang gagal.
- Hapus pernyataan
assertEquals
yang sudah ada. - Tambahkan kode untuk menguji bahwa satu-satunya tugas disediakan oleh sumber data tersebut dan tugas ini sama dengan yang dimasukkan.
Urutan parameter untuk assertEquals
harus selalu berupa nilai yang diharapkan, kemudian nilai sebenarnya**.**
assertEquals(1, tasks.size)
assertEquals(task, tasks[0])
- Jalankan pengujian lagi. Dalam jendela hasil pengujian, Anda akan melihat pengujian berhasil.
Gambar 13. Screenshot yang menunjukkan pengujian yang berhasil.
6. Membuat sumber data jaringan
Tugas yang dapat disimpan secara lokal di perangkat adalah hal yang bagus, tetapi bagaimana jika Anda juga ingin menyimpan dan memuat tugas tersebut ke layanan jaringan? Mungkin aplikasi Android Anda hanyalah salah satu cara yang dapat dipakai pengguna untuk menambahkan tugas ke daftar TODO mereka. Tugas juga dapat dikelola melalui situs atau aplikasi desktop. Atau, mungkin Anda hanya ingin menyediakan cadangan data online sehingga pengguna dapat memulihkan data aplikasi meskipun mereka mengganti perangkat mereka.
Dalam skenario ini, Anda biasanya memiliki layanan berbasis jaringan yang dapat digunakan semua klien, termasuk aplikasi Android Anda, untuk memuat dan menyimpan data.
Pada langkah berikutnya, Anda akan membuat sumber data untuk berkomunikasi dengan layanan jaringan ini. Untuk tujuan codelab ini, langkah ini adalah layanan simulasi yang tidak terhubung ke layanan jaringan live, tetapi memberi Anda gambaran tentang cara penerapannya di aplikasi nyata.
Tentang layanan jaringan
Dalam contoh, API jaringan sangat sederhana. API itu hanya menjalankan dua operasi:
- Menyimpan semua tugas, menimpa data yang ditulis sebelumnya.
- Memuat semua tugas, yang menyediakan daftar semua tugas yang saat ini disimpan di layanan jaringan.
Membuat model data jaringan
Saat mendapatkan data dari API jaringan, biasanya data tersebut direpresentasikan secara berbeda dari data lokal. Representasi jaringan tugas mungkin memiliki kolom tambahan, atau mungkin menggunakan jenis atau nama kolom yang berbeda untuk mewakili nilai yang sama.
Untuk memperhitungkan perbedaan ini, buat model data khusus untuk jaringan.
- Buka file
NetworkTask.kt
yang ada didata/source/network
, lalu tambahkan kode berikut untuk mewakili kolom:
data class NetworkTask(
val id: String,
val title: String,
val shortDescription: String,
val priority: Int? = null,
val status: TaskStatus = TaskStatus.ACTIVE
) {
enum class TaskStatus {
ACTIVE,
COMPLETE
}
}
Berikut ini perbedaan antara LocalTask
dan NetworkTask
:
- Deskripsi tugas diberi nama
shortDescription
, bukandescription
. - Kolom
isCompleted
direpresentasikan sebagai enumstatus
, yang memiliki dua kemungkinan nilai:ACTIVE
danCOMPLETE
. - Enum ini berisi kolom
priority
tambahan, yang merupakan bilangan bulat.
Membuat sumber data jaringan
- Buka
TaskNetworkDataSource.kt
, lalu buat class bernamaTaskNetworkDataSource
dengan konten berikut:
class TaskNetworkDataSource @Inject constructor() {
// A mutex is used to ensure that reads and writes are thread-safe.
private val accessMutex = Mutex()
private var tasks = listOf(
NetworkTask(
id = "PISA",
title = "Build tower in Pisa",
shortDescription = "Ground looks good, no foundation work required."
),
NetworkTask(
id = "TACOMA",
title = "Finish bridge in Tacoma",
shortDescription = "Found awesome girders at half the cost!"
)
)
suspend fun loadTasks(): List<NetworkTask> = accessMutex.withLock {
delay(SERVICE_LATENCY_IN_MILLIS)
return tasks
}
suspend fun saveTasks(newTasks: List<NetworkTask>) = accessMutex.withLock {
delay(SERVICE_LATENCY_IN_MILLIS)
tasks = newTasks
}
}
private const val SERVICE_LATENCY_IN_MILLIS = 2000L
Objek ini menyimulasikan interaksi dengan server, termasuk simulasi penundaan dua detik setiap kali loadTasks
atau saveTasks
dipanggil. Objek ini dapat mewakili latensi respons jaringan atau server.
Objek ini juga mencakup beberapa data pengujian yang Anda gunakan nanti untuk memastikan bahwa tugas berhasil dimuat dari jaringan.
Jika API server asli Anda menggunakan HTTP, pertimbangkan untuk menggunakan library seperti Ktor atau Retrofit untuk membangun sumber data jaringan Anda.
7. Membuat repositori tugas
Bagian-bagian telah menyatu.
Gambar 14. Diagram yang menunjukkan dependensi DefaultTaskRepository
.
Kita memiliki dua sumber data—satu untuk data lokal (TaskDao
) dan satu untuk data jaringan (TaskNetworkDataSource
). Masing-masing memungkinkan untuk membaca dan menulis, serta memiliki representasi tugasnya sendiri (LocalTask
dan NetworkTask
).
Sekarang saatnya membuat repositori yang menggunakan sumber data ini dan menyediakan API sehingga lapisan arsitektur lain dapat mengakses data tugas ini.
Mengekspos data
- Buka
DefaultTaskRepository.kt
di paketdata
, lalu buat class bernamaDefaultTaskRepository
, yang mengambilTaskDao
danTaskNetworkDataSource
sebagai dependensi.
class DefaultTaskRepository @Inject constructor(
private val localDataSource: TaskDao,
private val networkDataSource: TaskNetworkDataSource,
) {
}
Data harus diekspos menggunakan flow. Hal ini memungkinkan pemanggil diberi tahu tentang perubahan data tersebut dari waktu ke waktu.
- Tambahkan metode bernama
observeAll
, yang menampilkan aliran modelTask
menggunakanFlow
.
fun observeAll() : Flow<List<Task>> {
// TODO add code to retrieve Tasks
}
Repositori harus mengekspos data dari satu sumber kebenaran. Artinya, data harus berasal dari satu sumber data saja. Sumber ini bisa berupa cache dalam memori, server jarak jauh, atau, dalam hal ini, database lokal.
Tugas dalam database lokal dapat diakses menggunakan TaskDao.observeAll
, yang dengan mudah menampilkan flow. Tetapi, ini adalah flow model LocalTask
, dengan LocalTask
adalah model internal yang tidak boleh diekspos ke lapisan arsitektur lainnya.
Anda perlu mengubah LocalTask
menjadi Task
. Model ini adalah model eksternal yang merupakan bagian dari API lapisan data.
Memetakan model internal ke model eksternal
Untuk melakukan konversi ini, Anda perlu memetakan kolom dari LocalTask
ke kolom di Task
.
- Buat fungsi ekstensi untuk tindakan ini di
LocalTask
.
// Convert a LocalTask to a Task
fun LocalTask.toExternal() = Task(
id = id,
title = title,
description = description,
isCompleted = isCompleted,
)
// Convenience function which converts a list of LocalTasks to a list of Tasks
fun List<LocalTask>.toExternal() = map(LocalTask::toExternal) // Equivalent to map { it.toExternal() }
Sekarang, kapan pun Anda perlu mengubah LocalTask
menjadi Task
, Anda hanya perlu memanggil toExternal
.
- Gunakan fungsi
toExternal
yang baru Anda buat di dalamobserveAll
:
fun observeAll(): Flow<List<Task>> {
return localDataSource.observeAll().map { tasks ->
tasks.toExternal()
}
}
Setiap kali data tugas berubah di database lokal, daftar model LocalTask
baru akan ditampilkan ke dalam aliran. Kemudian, setiap LocalTask
dipetakan ke Task
.
Bagus. Sekarang lapisan lain dapat menggunakan observeAll
untuk mendapatkan semua model Task
dari database lokal Anda dan diberi tahu setiap kali model Task
tersebut berubah.
Memperbarui data
Aplikasi TODO tidak terlalu bagus jika Anda tidak dapat membuat dan memperbarui tugas. Anda sekarang menambahkan metode untuk melakukannya.
Metode untuk membuat, memperbarui, atau menghapus data adalah operasi sekali pakai, dan harus diimplementasikan menggunakan fungsi suspend
.
- Tambahkan metode bernama
create
, yang mengambiltitle
dandescription
sebagai parameter dan menampilkan ID tugas yang baru dibuat.
suspend fun create(title: String, description: String): String {
}
Perhatikan bahwa API lapisan data tidak memungkinkan Task
dibuat oleh lapisan lain dengan hanya menyediakan metode create
yang menerima parameter individual, bukan Task
. Pendekatan ini merangkum:
- Logika bisnis untuk membuat ID tugas unik.
- Tempat tugas disimpan setelah pembuatan awal.
- Tambahkan metode untuk membuat ID tugas
// This method might be computationally expensive
private fun createTaskId() : String {
return UUID.randomUUID().toString()
}
- Buat ID tugas menggunakan metode
createTaskId
yang baru ditambahkan
suspend fun create(title: String, description: String): String {
val taskId = createTaskId()
}
Jangan memblokir thread utama
Tapi tunggu! Bagaimana jika pembuatan ID tugas mahal secara komputasi? Mungkin prosesnya menggunakan kriptografi untuk membuat kunci hash bagi ID, yang memakan waktu beberapa detik. Hal ini dapat menyebabkan UI mengalami jank jika dipanggil di thread utama.
Lapisan data memiliki tanggung jawab untuk memastikan tugas yang rumit atau berjalan lama tidak memblokir thread utama.
Untuk memperbaikinya, tentukan operator coroutine yang akan digunakan untuk menjalankan instruksi ini.
- Pertama, tambahkan
CoroutineDispatcher
sebagai dependensi untukDefaultTaskRepository
. Gunakan penentu@DefaultDispatcher
yang sudah dibuat (didefinisikan dalamdi/CoroutinesModule.kt
) untuk memberi tahu Hilt agar memasukkan dependensi ini denganDispatchers.Default
. OperatorDefault
ditentukan karena dioptimalkan untuk tugas intensif CPU. Baca selengkapnya tentang dispatcher coroutine di sini.
class DefaultTaskRepository @Inject constructor(
private val localDataSource: TaskDao,
private val networkDataSource: TaskNetworkDataSource,
@DefaultDispatcher private val dispatcher: CoroutineDispatcher,
)
- Sekarang, tempatkan panggilan ke
UUID.randomUUID().toString()
di dalam blokwithContext
.
val taskId = withContext(dispatcher) {
createTaskId()
}
Baca selengkapnya tentang threading di lapisan data.
Membuat dan menyimpan tugas
- Sekarang setelah Anda memiliki ID tugas, gunakan bersama dengan parameter yang disediakan untuk membuat
Task
yang baru.
suspend fun create(title: String, description: String): String {
val taskId = withContext(dispatcher) {
createTaskId()
}
val task = Task(
title = title,
description = description,
id = taskId,
)
}
Sebelum memasukkan tugas ke sumber data lokal, Anda perlu memetakannya ke LocalTask
.
- Tambahkan fungsi ekstensi berikut ke akhir
LocalTask
. Fungsi ini adalah fungsi pemetaan terbalik keLocalTask.toExternal
, yang telah Anda buat sebelumnya.
fun Task.toLocal() = LocalTask(
id = id,
title = title,
description = description,
isCompleted = isCompleted,
)
- Gunakan fungsi ini di dalam
create
untuk memasukkan tugas ke sumber data lokal, lalu tampilkantaskId
.
suspend fun create(title: String, description: String): Task {
...
localDataSource.upsert(task.toLocal())
return taskId
}
Menyelesaikan tugas
- Buat metode tambahan,
complete
, yang menandaiTask
sebagai selesai.
suspend fun complete(taskId: String) {
localDataSource.updateCompleted(taskId, true)
}
Anda sekarang memiliki beberapa metode yang berguna untuk membuat dan menyelesaikan tugas.
Menyinkronkan data
Di aplikasi ini, sumber data jaringan digunakan sebagai cadangan online yang diperbarui setiap kali data ditulis secara lokal. Data dimuat dari jaringan setiap kali pengguna meminta pemuatan ulang.
Diagram berikut merangkum perilaku untuk setiap jenis operasi.
Jenis operasi | Metode repositori | Langkah | Pergerakan data |
Muat |
| Muat data dari database lokal | Gambar 15. Diagram yang menampilkan aliran data dari sumber data lokal ke repositori tugas. |
Simpan |
| 1. Tulis data ke local database2. Salin semua data ke jaringan, timpa semuanya | Gambar 16. Diagram yang menunjukkan aliran data dari repositori tugas ke sumber data lokal, kemudian ke sumber data jaringan. |
Muat ulang |
| 1. Muat data dari network2. Salin ke database lokal, timpa semuanya | Gambar 17. Diagram yang menunjukkan aliran data dari sumber data jaringan ke sumber data lokal, kemudian ke repositori tugas. |
Menyimpan dan memuat ulang data jaringan
Repositori Anda sudah memuat tugas dari sumber data lokal. Untuk menyelesaikan algoritma sinkronisasi, Anda perlu membuat metode untuk menyimpan dan memuat ulang data dari sumber data jaringan.
- Pertama, buat fungsi pemetaan dari
LocalTask
keNetworkTask
dan sebaliknya di dalamNetworkTask.kt
. Menempatkan fungsi di dalamLocalTask.kt
juga sama validnya.
fun NetworkTask.toLocal() = LocalTask(
id = id,
title = title,
description = shortDescription,
isCompleted = (status == NetworkTask.TaskStatus.COMPLETE),
)
fun List<NetworkTask>.toLocal() = map(NetworkTask::toLocal)
fun LocalTask.toNetwork() = NetworkTask(
id = id,
title = title,
shortDescription = description,
status = if (isCompleted) { NetworkTask.TaskStatus.COMPLETE } else { NetworkTask.TaskStatus.ACTIVE }
)
fun List<LocalTask>.toNetwork() = map(LocalTask::toNetwork)
Di sini Anda dapat melihat keuntungan memiliki model terpisah untuk setiap sumber data—pemetaan satu jenis data ke jenis data lainnya dienkapsulasi ke dalam fungsi terpisah.
- Tambahkan metode
refresh
di akhirDefaultTaskRepository
.
suspend fun refresh() {
val networkTasks = networkDataSource.loadTasks()
localDataSource.deleteAll()
val localTasks = withContext(dispatcher) {
networkTasks.toLocal()
}
localDataSource.upsertAll(networkTasks.toLocal())
}
Tindakan ini akan menggantikan semua tugas lokal dengan tugas dari jaringan. withContext
digunakan untuk operasi toLocal
massal karena ada tugas yang jumlahnya tidak diketahui, dan setiap operasi pemetaan mungkin mahal secara komputasi.
- Tambahkan metode
saveTasksToNetwork
di akhirDefaultTaskRepository
.
private suspend fun saveTasksToNetwork() {
val localTasks = localDataSource.observeAll().first()
val networkTasks = withContext(dispatcher) {
localTasks.toNetwork()
}
networkDataSource.saveTasks(networkTasks)
}
Tindakan ini akan menggantikan semua tugas jaringan dengan tugas dari sumber data lokal.
- Sekarang perbarui metode yang ada, yang memperbarui tugas
create
dancomplete
sehingga data lokal disimpan ke jaringan saat data itu berubah.
suspend fun create(title: String, description: String): String {
...
saveTasksToNetwork()
return taskId
}
suspend fun complete(taskId: String) {
localDataSource.updateCompleted(taskId, true)
saveTasksToNetwork()
}
Jangan membuat pemanggil menunggu
Jika Anda menjalankan kode ini, Anda akan melihat bahwa saveTasksToNetwork
memblokir. Hal ini berarti pemanggil dari create
dan complete
terpaksa menunggu sampai data disimpan ke jaringan sebelum pemanggil tersebut dapat memastikan bahwa operasi telah selesai. Dalam simulasi sumber data jaringan, waktu yang dibutuhkan hanya dua detik, tetapi dalam aplikasi sebenarnya mungkin lebih lama—atau tidak pernah jika tidak ada koneksi jaringan.
Kondisi ini terlalu membatasi dan kemungkinan besar akan menyebabkan pengalaman pengguna yang buruk—tidak ada yang mau menunggu untuk membuat tugas, terutama saat mereka sedang sibuk.
Solusi yang lebih baik adalah menggunakan cakupan coroutine yang berbeda untuk menyimpan data ke jaringan. Solusi ini memungkinkan operasi selesai di latar belakang tanpa membuat pemanggil menunggu hasilnya.
- Tambahkan cakupan coroutine sebagai parameter untuk
DefaultTaskRepository
.
class DefaultTaskRepository @Inject constructor(
// ...other parameters...
@ApplicationScope private val scope: CoroutineScope,
)
Penentu Hilt @ApplicationScope
(didefinisikan dalam di/CoroutinesModule.kt
) digunakan untuk memasukkan cakupan yang mengikuti siklus proses aplikasi.
- Gabungkan kode di dalam
saveTasksToNetwork
denganscope.launch
.
private fun saveTasksToNetwork() {
scope.launch {
val localTasks = localDataSource.observeAll().first()
val networkTasks = withContext(dispatcher) {
localTasks.toNetwork()
}
networkDataSource.saveTasks(networkTasks)
}
}
Sekarang saveTasksToNetwork
segera ditampilkan dan tugas disimpan ke jaringan di latar belakang.
8. Menguji repositori tugas
Wah, banyak sekali fungsi yang ditambahkan ke lapisan data Anda. Saatnya untuk memastikan semuanya berfungsi dengan membuat pengujian unit untuk DefaultTaskRepository
.
Anda perlu membuat instance subjek yang diuji (DefaultTaskRepository
) dengan dependensi uji untuk sumber data lokal dan jaringan. Pertama, Anda perlu membuat dependensi tersebut.
- Di jendela Project Explorer, perluas folder
(test)
, lalu perluas foldersource.local
dan bukaFakeTaskDao.kt.
Gambar 18. Screenshot yang menunjukkan FakeTaskDao.kt
dalam struktur folder Project.
- Tambahkan konten berikut:
class FakeTaskDao(initialTasks: List<LocalTask>) : TaskDao {
private val _tasks = initialTasks.toMutableList()
private val tasksStream = MutableStateFlow(_tasks.toList())
override fun observeAll(): Flow<List<LocalTask>> = tasksStream
override suspend fun upsert(task: LocalTask) {
_tasks.removeIf { it.id == task.id }
_tasks.add(task)
tasksStream.emit(_tasks)
}
override suspend fun upsertAll(tasks: List<LocalTask>) {
val newTaskIds = tasks.map { it.id }
_tasks.removeIf { newTaskIds.contains(it.id) }
_tasks.addAll(tasks)
}
override suspend fun updateCompleted(taskId: String, completed: Boolean) {
_tasks.firstOrNull { it.id == taskId }?.let { it.isCompleted = completed }
tasksStream.emit(_tasks)
}
override suspend fun deleteAll() {
_tasks.clear()
tasksStream.emit(_tasks)
}
}
Dalam aplikasi sebenarnya, Anda juga akan membuat dependensi palsu untuk mengganti TaskNetworkDataSource
(dengan membuat objek palsu dan objek asli mengimplementasikan antarmuka umum). Tetapi, untuk tujuan codelab ini, Anda akan menggunakannya secara langsung.
- Dalam
DefaultTaskRepositoryTest
, tambahkan hal berikut.
Aturan yang menetapkan operator utama untuk digunakan di semua pengujian. |
Beberapa data uji. |
Dependensi pengujian untuk sumber data lokal dan jaringan. |
Subjek yang diuji: |
class DefaultTaskRepositoryTest {
private var testDispatcher = UnconfinedTestDispatcher()
private var testScope = TestScope(testDispatcher)
private val localTasks = listOf(
LocalTask(id = "1", title = "title1", description = "description1", isCompleted = false),
LocalTask(id = "2", title = "title2", description = "description2", isCompleted = true),
)
private val localDataSource = FakeTaskDao(localTasks)
private val networkDataSource = TaskNetworkDataSource()
private val taskRepository = DefaultTaskRepository(
localDataSource = localDataSource,
networkDataSource = networkDataSource,
dispatcher = testDispatcher,
scope = testScope
)
}
Bagus. Sekarang Anda dapat mulai menulis pengujian unit. Ada tiga area utama yang harus Anda uji: baca, tulis, dan sinkronisasi data.
Menguji data yang diekspos
Berikut ini cara Anda menguji apakah repositori mengekspos data dengan benar. Pengujian diberikan dalam struktur kondisi, jika, maka. Contoh:
Kondisi | Sumber data lokal memiliki beberapa tugas yang sudah ada |
Jika | Aliran tugas diperoleh dari repositori menggunakan |
Maka | Item pertama dalam aliran tugas cocok dengan representasi eksternal tugas di sumber data lokal |
- Buat pengujian bernama
observeAll_exposesLocalData
dengan konten berikut:
@Test
fun observeAll_exposesLocalData() = runTest {
val tasks = taskRepository.observeAll().first()
assertEquals(localTasks.toExternal(), tasks)
}
Gunakan fungsi first
untuk mendapatkan item pertama dari aliran tugas.
Menguji pembaruan data
Selanjutnya, tulis pengujian yang memastikan tugas dibuat dan disimpan ke sumber data jaringan.
Kondisi | Database yang kosong |
Jika | Tugas dibuat dengan memanggil |
Maka | Tugas dibuat di sumber data lokal dan jaringan |
- Buat pengujian bernama
onTaskCreation_localAndNetworkAreUpdated
.
@Test
fun onTaskCreation_localAndNetworkAreUpdated() = testScope.runTest {
val newTaskId = taskRepository.create(
localTasks[0].title,
localTasks[0].description
)
val localTasks = localDataSource.observeAll().first()
assertEquals(true, localTasks.map { it.id }.contains(newTaskId))
val networkTasks = networkDataSource.loadTasks()
assertEquals(true, networkTasks.map { it.id }.contains(newTaskId))
}
Selanjutnya, pastikan bahwa saat tugas selesai, tugas itu ditulis dengan benar ke sumber data lokal dan disimpan ke sumber data jaringan.
Kondisi | Sumber data lokal berisi tugas |
Jika | Tugas itu diselesaikan dengan memanggil |
Maka | Data lokal dan data jaringan juga diperbarui |
- Buat pengujian bernama
onTaskCompletion_localAndNetworkAreUpdated
.
@Test
fun onTaskCompletion_localAndNetworkAreUpdated() = testScope.runTest {
taskRepository.complete("1")
val localTasks = localDataSource.observeAll().first()
val isLocalTaskComplete = localTasks.firstOrNull { it.id == "1" } ?.isCompleted
assertEquals(true, isLocalTaskComplete)
val networkTasks = networkDataSource.loadTasks()
val isNetworkTaskComplete =
networkTasks.firstOrNull { it.id == "1"} ?.status == NetworkTask.TaskStatus.COMPLETE
assertEquals(true, isNetworkTaskComplete)
}
Menguji pemuatan ulang data
Terakhir, uji apakah operasi pemuatan ulang berhasil.
Kondisi | Sumber data jaringan berisi data |
Jika |
|
Maka | data lokal sama dengan data jaringan |
- Buat pengujian bernama
onRefresh_localIsEqualToNetwork
@Test
fun onRefresh_localIsEqualToNetwork() = runTest {
val networkTasks = listOf(
NetworkTask(id = "3", title = "title3", shortDescription = "desc3"),
NetworkTask(id = "4", title = "title4", shortDescription = "desc4"),
)
networkDataSource.saveTasks(networkTasks)
taskRepository.refresh()
assertEquals(networkTasks.toLocal(), localDataSource.observeAll().first())
}
Selesai. Jalankan pengujian, dan semuanya harus berhasil.
9. Memperbarui lapisan UI
Sekarang Anda tahu bahwa lapisan data berfungsi. Saatnya menghubungkannya ke lapisan UI.
Memperbarui model tampilan untuk layar daftar tugas
Diawali dengan TasksViewModel
. Model ini adalah model tampilan untuk menampilkan layar pertama di aplikasi—daftar semua tugas yang sedang aktif.
- Buka class ini dan tambahkan
DefaultTaskRepository
sebagai parameter konstruktor.
@HiltViewModel
class TasksViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val taskRepository: DefaultTaskRepository,
) : ViewModel () { /* ... */ }
- Lakukan inisialisasi variabel
tasksStream
menggunakan repositori.
private val tasksStream = taskRepository.observeAll()
Model tampilan Anda sekarang memiliki akses ke semua tugas yang disediakan oleh repositori, dan akan menerima daftar tugas baru setiap kali data berubah—hanya dalam satu baris kode.
- Yang tersisa hanyalah menghubungkan tindakan pengguna ke metode yang sesuai di repositori. Temukan metode
complete
dan perbarui ke:
fun complete(task: Task, completed: Boolean) {
viewModelScope.launch {
if (completed) {
taskRepository.complete(task.id)
showSnackbarMessage(R.string.task_marked_complete)
} else {
...
}
}
}
- Lakukan hal yang sama dengan
refresh
.
fun refresh() {
_isLoading.value = true
viewModelScope.launch {
taskRepository.refresh()
_isLoading.value = false
}
}
Memperbarui model tampilan untuk layar tambahkan tugas
- Buka
AddEditTaskViewModel
dan tambahkanDefaultTaskRepository
sebagai parameter konstruktor, sama seperti yang Anda lakukan pada langkah sebelumnya.
class AddEditTaskViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val taskRepository: DefaultTaskRepository,
)
- Perbarui metode
create
menjadi yang berikut:
private fun createNewTask() = viewModelScope.launch {
taskRepository.create(uiState.value.title, uiState.value.description)
_uiState.update {
it.copy(isTaskSaved = true)
}
}
Menjalankan aplikasi
- Inilah saat yang Anda tunggu-tunggu—waktu untuk menjalankan aplikasi. Anda akan melihat layar yang menunjukkan Anda tidak memiliki tugas..
Gambar 19. Screenshot layar tugas aplikasi ketika tidak ada tugas.
- Ketuk tiga titik di pojok kanan atas, lalu tekan Refresh.
Gambar 20. Screenshot layar tugas aplikasi dengan menu tindakan ditampilkan.
Anda akan melihat indikator lingkaran berputar muncul selama dua detik, lalu tugas pengujian yang Anda tambahkan sebelumnya akan muncul.
Gambar 21. Screenshot layar tugas aplikasi dengan dua tugas ditampilkan.
- Sekarang ketuk tanda tambah di pojok kanan bawah untuk menambahkan tugas baru. Isi kolom judul dan deskripsi.
Gambar 22. Screenshot layar tambahkan tugas aplikasi.
- Ketuk tombol centang di pojok kanan bawah untuk menyimpan tugas.
Gambar 23. Screenshot layar tugas aplikasi setelah tugas ditambahkan.
- Centang kotak di samping tugas untuk menandai tugas sebagai selesai.
Gambar 24. Screenshot layar tugas aplikasi yang menampilkan tugas yang telah diselesaikan.
10. Selamat!
Anda telah berhasil membuat lapisan data untuk aplikasi.
Lapisan data membentuk bagian penting dari arsitektur aplikasi Anda. Bagian ini adalah fondasi tempat lapisan lain dapat dibangun. Jadi, melakukannya dengan benar memungkinkan aplikasi Anda menyesuaikan dengan kebutuhan pengguna dan bisnis Anda.
Yang telah Anda pelajari
- Peran lapisan data dalam arsitektur aplikasi Android.
- Cara membuat sumber dan model data.
- Peran repositori, dan caranya mengekspos data serta menyediakan metode sekali pakai untuk memperbarui data.
- Kapan harus mengganti operator coroutine, dan mengapa penting untuk melakukannya.
- Sinkronisasi data menggunakan beberapa sumber data.
- Cara membuat pengujian unit dan pengujian berinstrumen untuk class lapisan data umum.
Tantangan selanjutnya
Jika Anda menginginkan tantangan lain, terapkan fitur-fitur berikut:
- Aktifkan kembali tugas setelah ditandai sebagai selesai.
- Edit judul dan deskripsi tugas dengan mengetuknya.
Tidak ada instruksi yang diberikan. Semua terserah Anda. Jika Anda menemui kesulitan, lihat aplikasi yang berfungsi penuh di cabang main
.
git checkout main
Langkah berikutnya
Untuk mempelajari lebih lanjut lapisan data, lihat dokumentasi resmi dan panduan untuk aplikasi offline-first. Anda juga dapat mempelajari lapisan arsitektur lainnya, lapisan UI dan lapisan domain.
Untuk contoh di dunia nyata yang lebih kompleks, lihat aplikasi Now in Android.