Lapisan data

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.

Dalam arsitektur standar, repositori lapisan data menyediakan data
    ke seluruh aplikasi dan bergantung pada sumber data.
Gambar 1. Peran lapisan UI dalam arsitektur aplikasi.

Class repositori bertanggung jawab untuk 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, atau Completable.
  • 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.

Dalam contoh, UserRepository bergantung pada dua class repositori lainnya:
    LoginRepository yang bergantung pada sumber data login lainnya; dan
    RegistrationRepository yang bergantung pada sumber data pendaftaran lainnya.
Gambar 2. Grafik dependensi repositori yang bergantung pada repositori lain.

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 pencakupan di Android, lihat postingan blog Cakupan di Android dan Hilt.

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. Untuk error yang dapat dipicu oleh fungsi penangguhan, gunakan blok try/catch jika sesuai; dan dalam flow, gunakan operator catch. 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 di coroutine lebih lanjut, lihat postingan blog Pengecualian di coroutine.

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 pembacaan dan penulisan dari thread yang berbeda, digunakan Mutex. 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 postingan blog ini 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 library populer seperti WireMock atau MockWebServer yang memungkinkan Anda memalsukan panggilan HTTP dan HTTPS serta memverifikasi bahwa permintaan dibuat seperti yang diharapkan.

Contoh

Contoh Google berikut menunjukkan penggunaan lapisan data. Jelajahi untuk melihat panduan ini dalam praktik: