Lapisan UI berisi status terkait UI dan logika UI, sementara lapisan data berisi data aplikasi dan logika bisnis. Logika bisnis adalah yang memberikan nilai bagi aplikasi Anda. Logika ini adalah aturan bisnis di dunia nyata yang menentukan cara data aplikasi harus dibuat, disimpan, dan diubah.
Pemisahan fokus ini memungkinkan lapisan data digunakan di beberapa layar, membagikan informasi di antara berbagai bagian aplikasi, dan mereproduksi logika bisnis di luar UI untuk pengujian unit. Untuk informasi selengkapnya tentang manfaat lapisan data, lihat halaman Ringkasan Arsitektur.
Arsitektur lapisan data
Lapisan data terdiri dari repositori yang masing-masing dapat berisi nol hingga banyak
sumber data. Anda harus membuat class repositori untuk setiap jenis data
yang ditangani di aplikasi Anda. Misalnya, Anda mungkin membuat class MoviesRepository
untuk data yang berhubungan dengan film, atau class PaymentsRepository
untuk data
yang terkait dengan pembayaran.
Class repositori bertanggung jawab atas tugas-tugas berikut:
- Mengekspos data ke seluruh aplikasi.
- Memusatkan perubahan pada data.
- Menyelesaikan konflik antara beberapa sumber data.
- Mengabstraksi sumber data dari bagian aplikasi lainnya.
- Berisi logika bisnis.
Setiap class sumber data harus memiliki tanggung jawab untuk menangani hanya satu sumber data, yang dapat berupa file, sumber jaringan, atau database lokal. Class sumber data adalah jembatan antara aplikasi dan sistem untuk operasi data.
Lapisan lain dalam hierarki tidak boleh mengakses sumber data secara langsung; titik entri ke lapisan data selalu berupa class repositori. Class holder status (lihat panduan lapisan UI) atau class kasus penggunaan (lihat panduan lapisan domain) tidak boleh memiliki sumber data sebagai dependensi langsung. Penggunaan class repositori sebagai titik entri memungkinkan berbagai lapisan arsitektur menyesuaikan skala secara independen.
Data yang diekspos oleh lapisan ini tidak dapat diubah sehingga tidak dapat dirusak oleh class lain, yang akan berisiko menempatkan nilainya dalam status tidak konsisten. Data yang tidak dapat diubah juga dapat ditangani dengan aman oleh beberapa thread. Lihat bagian threading untuk mengetahui detail selengkapnya.
Sesuai praktik terbaik injeksi dependensi, repositori mengambil sumber data sebagai dependensi dalam konstruktornya:
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
Mengekspos API
Class di lapisan data umumnya mengekspos fungsi untuk melakukan panggilan Pembuatan, Pembacaan, Update, dan Penghapusan (CRUD) satu kali atau untuk mendapatkan notifikasi perubahan data dari waktu ke waktu. Lapisan data harus menampilkan hal berikut untuk setiap kasus berikut:
- Operasi satu kali: Lapisan data harus mengekspos fungsi penangguhan di
Kotlin; dan untuk bahasa pemrograman Java, lapisan data harus mengekspos
fungsi yang menyediakan callback untuk memberi tahu hasil operasi, atau
jenis RxJava
Single
,Maybe
, atauCompletable
. - Agar diberi tahu tentang perubahan data dari waktu ke waktu: Lapisan data harus mengekspos
alur di Kotlin; dan untuk bahasa pemrograman Java, lapisan
data harus mengekspos callback yang memunculkan data baru, atau RxJava
Observable
atauFlowable
.
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
suspend fun modifyData(example: Example) { ... }
}
Konvensi penamaan dalam panduan ini
Dalam panduan ini, class repositori diberi nama berdasarkan data yang menjadi tanggung jawabnya. Konvensinya adalah sebagai berikut:
jenis data + Repository.
Misalnya: NewsRepository
, MoviesRepository
, atau PaymentsRepository
.
Class sumber data diberi nama sesuai data yang menjadi tanggung jawabnya dan sumber yang digunakannya. Konvensinya adalah sebagai berikut:
jenis data + jenis sumber + DataSource.
Untuk jenis data, gunakan Remote atau Local agar lebih umum karena
implementasi dapat berubah. Misalnya: NewsRemoteDataSource
atau
NewsLocalDataSource
. Untuk lebih spesifik jika sumber penting, gunakan
jenis sumber. Misalnya: NewsNetworkDataSource
atau
NewsDiskDataSource
.
Jangan beri nama sumber data berdasarkan detail implementasi—misalnya,
UserSharedPreferencesDataSource
—karena repositori yang menggunakan sumber data tersebut
tidak boleh mengetahui cara data disimpan. Jika mengikuti aturan ini, Anda dapat mengubah
implementasi sumber data (misalnya, bermigrasi dari
SharedPreferences ke
DataStore) tanpa memengaruhi lapisan
yang memanggil sumber tersebut.
Beberapa level repositori
Dalam beberapa kasus yang melibatkan persyaratan bisnis yang lebih kompleks, repositori mungkin perlu bergantung pada repositori lainnya. Ini mungkin karena data yang terlibat adalah agregasi dari beberapa sumber data, atau karena tanggung jawab perlu dienkapsulasi dalam class repositori lain.
Misalnya, repositori yang menangani data autentikasi pengguna,
UserRepository
, dapat bergantung pada repositori lain seperti LoginRepository
dan RegistrationRepository
untuk memenuhi persyaratannya.
Sumber kebenaran
Penting bahwa setiap repositori mendefinisikan satu sumber kebenaran. Sumber kebenaran selalu berisi data yang konsisten, benar, dan terbaru. Bahkan, data yang ditampilkan dari repositori ini harus selalu berupa data yang berasal langsung dari sumber tepercaya.
Sumber tepercaya dapat berupa sumber data—misalnya, database—atau bahkan cache dalam memori yang mungkin terdapat dalam repositori. Repositori menggabungkan berbagai sumber data dan menyelesaikan setiap potensi konflik di antara sumber data untuk mengupdate satu sumber tepercaya secara teratur atau karena peristiwa input pengguna.
Repositori berbeda yang ada di aplikasi Anda mungkin memiliki sumber kebenaran yang berbeda. Misalnya,
class LoginRepository
mungkin menggunakan cache-nya sebagai sumber kebenaran
dan class PaymentsRepository
mungkin menggunakan sumber data jaringan.
Untuk memberikan dukungan offline terlebih dahulu, sumber data lokal—seperti database—adalah sumber kebenaran yang direkomendasikan.
Threading
Pemanggilan sumber data dan repositori harus bersifat main-safe, yaitu aman untuk dipanggil dari thread utama. Class ini bertanggung jawab memindahkan eksekusi logikanya ke thread yang sesuai saat melakukan operasi pemblokiran yang berjalan lama. Misalnya, sumber data harus aman untuk dibaca dari file, atau agar repositori melakukan pemfilteran berbiaya tinggi pada daftar besar.
Perlu diperhatikan bahwa sebagian besar sumber data sudah menyediakan API main-safe seperti panggilan metode penangguhan yang disediakan oleh Room, Retrofit, atau Ktor. Repositori Anda dapat memanfaatkan API ini jika tersedia.
Untuk mempelajari threading lebih lanjut, lihat panduan pemrosesan latar belakang. Untuk pengguna Kotlin, sebaiknya gunakan coroutine. Lihat Menjalankan tugas Android di thread latar belakang sebagai opsi yang direkomendasikan untuk bahasa pemrograman Java.
Lifecycle
Instance class dalam lapisan data tetap berada dalam memori selama instance dapat diakses dari root pembersihan sampah memori—biasanya dengan direferensikan dari objek lain dalam aplikasi Anda.
Jika class berisi data dalam memori, misalnya cache, Anda dapat menggunakan kembali instance class yang sama untuk periode waktu tertentu. Class ini juga disebut sebagai lifecycle instance class.
Jika class memiliki tanggung jawab yang sangat penting untuk seluruh aplikasi, Anda dapat
memberi cakupan pada instance class tersebut ke class Application
. Ini menjadikannya demikian
sehingga instance mengikuti lifecycle aplikasi. Atau, jika Anda hanya
perlu menggunakan kembali instance yang sama dalam alur tertentu di aplikasi Anda—misalnya,
alur pendaftaran atau login—Anda harus mencakupkan instance tersebut ke class
yang memiliki lifecycle alur tersebut. Misalnya, Anda dapat mencakupkan
RegistrationRepository
yang berisi data dalam memori ke
RegistrationActivity
atau grafik
navigasi
alur pendaftaran Domain.
Lifecycle setiap instance adalah faktor penting dalam menentukan cara menyediakan dependensi dalam aplikasi. Sebaiknya ikuti praktik terbaik injeksi dependensi tempat dependensi dikelola dan dapat dicakup ke penampung dependensi. Untuk mempelajari lebih lanjut tentang di Android, lihat Cakupan di Android dan Hilt postingan blog kita.
Merepresentasikan model bisnis
Model data yang ingin ditampilkan dari lapisan data mungkin merupakan subkumpulan informasi yang didapatkan dari berbagai sumber data. Idealnya, sumber data yang berbeda—baik jaringan maupun lokal—hanya akan menampilkan informasi yang diperlukan aplikasi; tetapi, hal itu jarang terjadi.
Misalnya, bayangkan sebuah server News API yang tidak hanya menampilkan informasi artikel, tetapi juga mengedit histori, komentar pengguna, dan beberapa metadata:
data class ArticleApiModel(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val modifications: Array<ArticleApiModel>,
val comments: Array<CommentApiModel>,
val lastModificationDate: Date,
val authorId: Long,
val authorName: String,
val authorDateOfBirth: Date,
val readTimeMin: Int
)
Aplikasi tidak memerlukan informasi artikel sebanyak itu karena aplikasi hanya
menampilkan konten artikel di layar, bersama dengan informasi dasar
tentang penulisnya. Ini adalah praktik yang baik untuk memisahkan class model dan
repositori Anda hanya mengekspos data yang diperlukan oleh lapisan
lain hierarkinya. Misalnya, berikut cara memotong ArticleApiModel
dari
jaringan untuk mengekspos class model Article
ke lapisan
domain dan UI:
data class Article(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val authorName: String,
val readTimeMin: Int
)
Memisahkan class model akan memberikan manfaat jika dilakukan dengan cara berikut:
- Teknologi ini menghemat memori aplikasi dengan mengurangi data hanya untuk kebutuhan yang diperlukan.
- Cara ini akan menyesuaikan jenis data eksternal dengan jenis data yang digunakan oleh aplikasi—misalnya, aplikasi mungkin menggunakan jenis data berbeda untuk mewakili tanggal.
- Selain itu, cara ini juga memberikan pemisahan fokus yang lebih baik—misalnya, anggota besar dapat bekerja secara individual di lapisan jaringan dan UI fitur jika class model ditentukan sebelumnya.
Anda dapat memperluas praktik ini dan menentukan class model terpisah di bagian lain arsitektur aplikasi—misalnya, dalam class sumber data dan ViewModel. Namun, cara ini mengharuskan Anda menentukan class dan logika tambahan yang harus didokumentasi dan diuji dengan benar. Setidaknya, Anda sebaiknya membuat model baru dalam kasus apa pun ketika sumber data menerima data yang tidak sesuai dengan ekspektasi aplikasi lainnya.
Jenis operasi data
Lapisan data dapat menangani jenis operasi yang bervariasi berdasarkan seberapa penting operasi tersebut: operasi berorientasi UI, berorientasi aplikasi, dan berorientasi bisnis.
Operasi berorientasi UI
Operasi berorientasi UI hanya relevan saat pengguna berada di layar tertentu, dan dibatalkan saat pengguna keluar dari layar tersebut. Contohnya adalah menampilkan beberapa data yang diperoleh dari database.
Operasi berorientasi UI biasanya dipicu oleh lapisan UI dan mengikuti lifecycle pemanggil—misalnya, lifecycle ViewModel. Lihat bagian Membuat permintaan jaringan untuk mengetahui contoh operasi berorientasi UI.
Operasi berorientasi aplikasi
Operasi berorientasi aplikasi relevan selama aplikasi terbuka. Operasi ini akan dibatalkan jika aplikasi ditutup atau prosesnya dihentikan, Contohnya adalah menyimpan hasil permintaan jaringan ke dalam cache sehingga dapat digunakan nanti jika diperlukan. Lihat bagian Mengimplementasikan cache data dalam memori untuk mempelajari lebih lanjut.
Operasi ini biasanya mengikuti lifecycle class Application
atau
lapisan data. Sebagai contoh, lihat bagian Membuat operasi aktif lebih lama dari
waktu aktif layar.
Operasi yang berorientasi bisnis
Operasi yang berorientasi bisnis tidak dapat dibatalkan. Operasi ini harus tetap bertahan saat terjadi penghentian. Contohnya adalah menyelesaikan upload foto yang ingin diposting pengguna ke profilnya.
Rekomendasi untuk operasi berorientasi bisnis adalah menggunakan WorkManager. Untuk mempelajari lebih lanjut lihat bagian Menjadwalkan tugas menggunakan WorkManager.
Mengekspos error
Interaksi dengan repositori dan sumber data dapat berhasil atau mengembalikan
pengecualian jika terjadi kegagalan. Untuk coroutine dan alur, Anda harus menggunakan
mekanisme penanganan error
bawaan Kotlin. Sebagai
error yang dapat dipicu oleh fungsi penangguhan, gunakan blok try/catch
saat
sesuai; dan dalam alur, gunakan
catch
operator. Dengan pendekatan ini, lapisan UI diharapkan mampu menangani pengecualian saat
memanggil lapisan data.
Lapisan data dapat memahami dan menangani berbagai jenis error serta menampilkannya
menggunakan pengecualian kustom—misalnya, UserNotAuthenticatedException
.
Untuk mempelajari error dalam coroutine lebih lanjut, lihat Pengecualian di coroutine postingan blog kita.
Tugas umum
Bagian berikut menampilkan contoh cara menggunakan dan merancang lapisan data untuk menjalankan tugas tertentu yang umum dilakukan di aplikasi Android. Contoh ini didasarkan pada aplikasi Berita standar yang disebutkan sebelumnya dalam panduan ini.
Membuat permintaan jaringan
Membuat permintaan jaringan adalah salah satu tugas paling umum yang mungkin
dilakukan oleh aplikasi Android. Aplikasi Berita harus menyajikan berita terbaru
yang diambil dari jaringan kepada pengguna. Oleh karena itu, aplikasi memerlukan class sumber data untuk mengelola
operasi jaringan: NewsRemoteDataSource
. Untuk menampilkan informasi ke
bagian lain aplikasi, repositori baru yang menangani operasi pada data berita
akan dibuat: NewsRepository
.
Persyaratannya adalah berita terbaru harus diperbarui saat pengguna membuka layar. Dengan demikian, operasi ini adalah operasi berorientasi UI.
Membuat sumber data
Sumber data harus mengekspos fungsi yang menampilkan berita terbaru
daftar ArticleHeadline
instance. Sumber data harus menyediakan cara utama yang aman
untuk mendapatkan berita terbaru dari jaringan. Untuk itu, sumber data harus menjalankan dependensi pada CoroutineDispatcher
atau Executor
untuk menjalankan tugas.
Pembuatan permintaan jaringan adalah panggilan satu kali yang ditangani oleh metode fetchLatestNews()
baru:
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
/**
* Fetches the latest news from the network and returns the result.
* This executes on an IO-optimized thread pool, the function is main-safe.
*/
suspend fun fetchLatestNews(): List<ArticleHeadline> =
// Move the execution to an IO-optimized thread since the ApiService
// doesn't support coroutines and makes synchronous requests.
withContext(ioDispatcher) {
newsApi.fetchLatestNews()
}
}
// Makes news-related network synchronous requests.
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
Antarmuka NewsApi
menyembunyikan implementasi klien API jaringan dan hal ini
tidak membuat perbedaan apakah antarmuka didukung oleh
Retrofit atau
HttpURLConnection
. Mengandalkan
antarmuka akan membuat implementasi API dapat diganti di aplikasi.
Membuat repositori
Karena tidak ada logika tambahan yang diperlukan dalam class repositori untuk tugas ini,
NewsRepository
akan bertindak sebagai proxy untuk sumber data jaringan. Manfaat
menambahkan lapisan abstraksi tambahan ini akan dijelaskan di bagian cache
dalam memori.
// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
Untuk mempelajari cara menggunakan class repositori langsung dari lapisan UI, lihat panduan lapisan UI.
Mengimplementasikan cache data dalam memori
Misalnya, persyaratan baru diperkenalkan untuk aplikasi Berita: saat pengguna membuka layar, berita yang disimpan dalam cache harus ditampilkan kepada pengguna jika permintaan telah dibuat sebelumnya. Jika tidak, aplikasi harus membuat permintaan jaringan untuk mengambil berita terbaru.
Dengan persyaratan baru, aplikasi harus menyimpan berita terbaru di memori saat pengguna membuka aplikasi. Jadi, operasi ini adalah operasi berorientasi aplikasi.
Cache
Anda dapat menyimpan data saat pengguna berada di aplikasi dengan menambahkan caching data dalam memori. Cache dimaksudkan untuk menyimpan beberapa informasi di memori selama jangka waktu tertentu—dalam hal ini, selama pengguna berada dalam aplikasi. Implementasi cache dapat berupa berbagai bentuk. Fungsi ini dapat bervariasi, mulai dari variabel sederhana yang dapat berubah hingga class yang lebih rumit, yang melindungi dari operasi baca/tulis di beberapa thread. Bergantung pada kasus penggunaan, caching dapat diterapkan di repositori atau di class sumber data.
Menyimpan hasil permintaan jaringan ke dalam cache
Untuk mempermudah, NewsRepository
menggunakan variabel yang dapat diubah untuk meng-cache berita
terbaru. Untuk melindungi operasi baca dan tulis dari thread yang berbeda, sebuah
Mutex
digunakan. Untuk mempelajari lebih lanjut status dapat berubah
dan konkurensi bersama, lihat
dokumentasi Kotlin.
Implementasi berikut menyimpan cache informasi berita terbaru ke variabel dalam
repositori yang diproteksi tulis dengan Mutex
. Jika hasil permintaan jaringan
berhasil, data akan ditetapkan ke variabel latestNews
.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Mutex to make writes to cached values thread-safe.
private val latestNewsMutex = Mutex()
// Cache of the latest news got from the network.
private var latestNews: List<ArticleHeadline> = emptyList()
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
if (refresh || latestNews.isEmpty()) {
val networkResult = newsRemoteDataSource.fetchLatestNews()
// Thread-safe write to latestNews
latestNewsMutex.withLock {
this.latestNews = networkResult
}
}
return latestNewsMutex.withLock { this.latestNews }
}
}
Membuat operasi aktif lebih lama daripada layar
Jika pengguna keluar dari layar saat permintaan jaringan
sedang berlangsung, permintaan akan dibatalkan dan hasilnya tidak akan disimpan dalam cache. NewsRepository
tidak boleh menggunakan CoroutineScope
pemanggil untuk melakukan logika ini. Sebagai gantinya,
NewsRepository
harus menggunakan CoroutineScope
yang disertakan ke lifecycle=nya.
Pengambilan berita terbaru harus berupa operasi berorientasi aplikasi.
Untuk mengikuti praktik terbaik injeksi dependensi, NewsRepository
harus menerima
cakupan sebagai parameter dalam konstruktornya, dan tidak membuat
CoroutineScope
-nya sendiri. Karena repositori harus melakukan sebagian besar pekerjaannya di
thread latar belakang, Anda harus mengonfigurasi CoroutineScope
dengan
Dispatchers.Default
atau dengan kumpulan thread Anda sendiri.
class NewsRepository(
...,
// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
private val externalScope: CoroutineScope
) { ... }
NewsRepository
telah siap melakukan operasi berorientasi aplikasi
dengan fungsi eksternal CoroutineScope
sehingga fungsi harus melakukan panggilan ke sumber data dan menyimpan
hasilnya dengan coroutine baru yang dimulai dengan cakupan tersebut:
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val externalScope: CoroutineScope
) {
/* ... */
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
return if (refresh) {
externalScope.async {
newsRemoteDataSource.fetchLatestNews().also { networkResult ->
// Thread-safe write to latestNews.
latestNewsMutex.withLock {
latestNews = networkResult
}
}
}.await()
} else {
return latestNewsMutex.withLock { this.latestNews }
}
}
}
async
digunakan untuk memulai coroutine dalam cakupan eksternal. await
dipanggil di
coroutine baru untuk ditangguhkan hingga permintaan jaringan kembali dan
hasilnya disimpan ke cache. Jika pada saat itu pengguna masih ada di layar,
pengguna akan melihat berita terbaru. Jika pengguna keluar dari layar,
await
akan dibatalkan, tetapi logika di dalam async
terus dijalankan.
Lihat blog ini
postingan
untuk mempelajari pola CoroutineScope
lebih lanjut.
Menyimpan dan mengambil data dari disk
Misalnya Anda ingin menyimpan data seperti berita yang diberi bookmark dan preferensi pengguna. Jenis data ini harus tetap bertahan saat terjadi penghentian proses dan dapat diakses meskipun pengguna tidak terhubung ke jaringan.
Jika data yang digunakan harus bertahan dalam penghentian proses, Anda harus menyimpannya di disk dengan salah satu cara berikut:
- Untuk set data besar yang harus dikueri, memerlukan integritas referensial, atau memerlukan parsial, simpan data di Database Room. Dalam contoh aplikasi Berita, artikel atau penulis berita dapat disimpan dalam database.
- Untuk set data kecil yang hanya perlu diambil dan ditetapkan (bukan kueri atau diperbarui sebagian), gunakan DataStore. Dalam contoh aplikasi Berita, format tanggal pilihan pengguna atau preferensi tampilan lainnya dapat disimpan di DataStore.
- Untuk bagian data seperti objek JSON, gunakan file.
Seperti yang disebutkan di bagian Sumber kebenaran, setiap sumber
data berfungsi hanya dengan satu sumber dan sesuai dengan jenis data tertentu (misalnya,
News
, Authors
, NewsAndAuthors
, atau UserPreferences
). Class
yang menggunakan sumber data seharusnya tidak mengetahui cara data disimpan—misalnya, dalam
database atau dalam file.
Room sebagai sumber data
Karena setiap sumber data memiliki tanggung jawab untuk bekerja hanya dengan satu
sumber untuk jenis data tertentu,
sumber data Room akan menerima objek akses data (DAO) atau
database itu sendiri sebagai parameter. Misalnya, NewsLocalDataSource
mungkin menggunakan
instance NewsDao
sebagai parameter, dan AuthorsLocalDataSource
mungkin mengambil
instance AuthorsDao
.
Dalam beberapa kasus, jika logika tambahan tidak diperlukan, Anda dapat memasukkan DAO secara langsung ke dalam repositori, karena DAO adalah antarmuka yang dapat diganti dengan mudah dalam pengujian.
Untuk mempelajari lebih lanjut cara menggunakan Room API, lihat Panduan Room.
DataStore sebagai sumber data
DataStore sangat cocok untuk menyimpan key-value pair seperti setelan pengguna. Contohnya meliputi format waktu, preferensi notifikasi, dan apakah akan menampilkan atau menyembunyikan item berita setelah pengguna membacanya. DataStore juga dapat menyimpan objek yang diketik dengan buffering protokol.
Seperti objek lainnya, sumber data yang didukung oleh DataStore harus berisi data yang sesuai dengan jenis tertentu atau ke bagian aplikasi tertentu. Situasi ini bahkan lebih sesuai dengan DataStore, karena operasi baca DataStore diekspos sebagai alur yang muncul setiap kali nilai diperbarui. Karena itu, Anda harus menyimpan preferensi terkait di DataStore yang sama.
Misalnya, Anda bisa memiliki NotificationsDataStore
yang hanya
menangani preferensi terkait notifikasi dan NewsPreferencesDataStore
yang hanya
menangani preferensi yang terkait dengan layar berita. Dengan begitu, Anda dapat menentukan cakupan update dengan lebih baik, karena alur newsScreenPreferencesDataStore.data
hanya akan muncul saat preferensi yang terkait dengan layar tersebut berubah. Hal ini juga berarti bahwa
siklus proses objek bisa lebih singkat karena hanya dapat aktif selama
layar berita ditampilkan.
Untuk mempelajari lebih lanjut cara menggunakan DataStore API, lihat Panduan DataStore.
File sebagai sumber data
Saat menangani objek besar seperti objek JSON atau bitmap, Anda harus
menggunakan objek File
dan menangani pengalihan thread.
Untuk mempelajari lebih lanjut cara menggunakan penyimpanan file, lihat halaman Ringkasan penyimpanan.
Menjadwalkan tugas menggunakan WorkManager
Misalkan persyaratan baru lainnya diperkenalkan untuk aplikasi Berita: aplikasi tersebut harus memberi pengguna opsi untuk mengambil berita terbaru secara rutin dan otomatis selama perangkat mengisi daya dan terhubung ke jaringan tidak berbayar. Proses ini membuat operasi ini berorientasi bisnis. Persyaratan ini menjadikannya demikian agar meskipun perangkat tidak memiliki konektivitas saat pengguna membuka aplikasi, pengguna masih dapat melihat berita terbaru.
WorkManager memudahkan penjadwalan
pekerjaan yang asinkron dan andal serta dapat menangani pengelolaan
batasan. Library ini direkomendasikan untuk pekerjaan yang bersifat persisten. Untuk menjalankan
tugas yang ditentukan di
atas, class Worker
akan dibuat: RefreshLatestNewsWorker
. Class ini menggunakan NewsRepository
sebagai dependensi untuk mengambil berita terbaru dan meng-cache-nya ke disk.
class RefreshLatestNewsWorker(
private val newsRepository: NewsRepository,
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
newsRepository.refreshLatestNews()
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
Logika bisnis untuk jenis tugas ini harus digabungkan dalam class-nya sendiri dan diperlakukan sebagai sumber data terpisah. WorkManager hanya akan bertanggung jawab untuk memastikan pekerjaan dijalankan pada thread latar belakang jika semua batasan terpenuhi. Dengan mematuhi pola ini, Anda dapat dengan cepat menukar implementasi di lingkungan yang berbeda sesuai kebutuhan.
Dalam contoh ini, tugas terkait berita ini harus dipanggil dari NewsRepository
yang akan mengambil sumber data baru sebagai dependensi: NewsTasksDataSource
dan diterapkan sebagai berikut:
private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"
class NewsTasksDataSource(
private val workManager: WorkManager
) {
fun fetchNewsPeriodically() {
val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
REFRESH_RATE_HOURS, TimeUnit.HOURS
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
.setRequiresCharging(true)
.build()
)
.addTag(TAG_FETCH_LATEST_NEWS)
workManager.enqueueUniquePeriodicWork(
FETCH_LATEST_NEWS_TASK,
ExistingPeriodicWorkPolicy.KEEP,
fetchNewsRequest.build()
)
}
fun cancelFetchingNewsPeriodically() {
workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
}
}
Jenis class ini diberi nama berdasarkan data yang menjadi tanggung jawabnya—misalnya,
NewsTasksDataSource
atau PaymentsTasksDataSource
. Semua tugas yang terkait
dengan jenis data tertentu harus dienkapsulasi di class yang sama.
Jika tugas perlu dipicu saat aplikasi dimulai, sebaiknya picu
permintaan WorkManager menggunakan library Aplikasi Startup
yang memanggil repositori dari
Initializer
.
Untuk mempelajari penggunaan WorkManager API lebih lanjut, lihat panduan WorkManager.
Pengujian
Praktik terbaik injeksi dependensi berguna saat menguji aplikasi. Sebaiknya Anda juga mengandalkan antarmuka untuk class yang berkomunikasi dengan resource eksternal. Saat menguji unit, Anda dapat memasukkan versi palsu dependensinya untuk membuat pengujian menjadi bersifat deterministik dan dapat diandalkan.
Pengujian unit
Panduan pengujian umum berlaku saat Anda menguji lapisan data. Untuk pengujian unit, gunakan objek asli jika diperlukan dan palsukan dependensi yang menjangkau sumber eksternal seperti membaca dari file atau membaca dari jaringan.
Pengujian integrasi
Pengujian integrasi yang mengakses sumber eksternal cenderung bersifat kurang deterministik karena harus berjalan di perangkat sungguhan. Sebaiknya jalankan pengujian tersebut dalam lingkungan yang terkontrol untuk membuat pengujian integrasi menjadi lebih andal.
Untuk database, Room memungkinkan pembuatan database dalam memori yang dapat Anda kontrol sepenuhnya dalam pengujian. Untuk mempelajari lebih lanjut, baca halaman Menguji dan men-debug database.
Untuk jaringan, ada perpustakaan ({i>library<i}) populer seperti WireMock atau MockWebServer yang memungkinkan Anda memalsukan panggilan HTTP dan HTTPS dan memverifikasi bahwa permintaan dibuat sebagai yang diharapkan.
Contoh
Contoh Google berikut menunjukkan penggunaan lapisan data. Jelajahi untuk melihat panduan ini dalam praktik:
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Lapisan domain
- Membangun aplikasi yang mengutamakan versi offline
- Produksi Status UI