1. Sebelum memulai
Pengantar
Pada codelab sebelumnya, Anda telah mempelajari cara mendapatkan data dari layanan web dengan meminta ViewModel
mengambil URL foto Mars dari jaringan menggunakan layanan API. Meskipun berhasil dan mudah diterapkan, pendekatan ini tidak diskalakan dengan baik seiring berkembangnya aplikasi Anda dan harus digunakan dengan lebih dari satu sumber data. Untuk mengatasi masalah ini, praktik terbaik arsitektur Android merekomendasikan pemisahan lapisan UI dan lapisan data.
Dalam codelab ini, Anda akan memfaktorkan ulang aplikasi Mars Photos menjadi lapisan data dan UI terpisah. Anda akan mempelajari cara menerapkan pola repositori dan menggunakan injeksi dependensi. Injeksi dependensi menciptakan struktur coding yang lebih fleksibel yang membantu pengembangan dan pengujian.
Prasyarat
- Mampu mengambil JSON dari layanan web REST dan mengurai data tersebut menjadi objek Kotlin menggunakan library Retrofit dan Serialization (kotlinx.serialization).
- Mengetahui cara menggunakan layanan web REST.
- Dapat menerapkan coroutine dalam aplikasi Anda.
Yang akan Anda pelajari
- Pola repositori
- Injeksi dependensi
Yang akan Anda bangun
- Memodifikasi aplikasi Mars Photos untuk memisahkan aplikasi menjadi lapisan UI dan lapisan data.
- Saat memisahkan lapisan data, Anda akan menerapkan pola repositori.
- Gunakan injeksi dependensi untuk membuat codebase yang dikaitkan secara longgar.
Yang Anda perlukan
- Komputer dengan browser web modern, seperti Chrome versi terbaru
Mendapatkan kode awal
Untuk memulai, download kode awal:
Atau, Anda dapat membuat clone repositori GitHub untuk kode tersebut:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git $ cd basic-android-kotlin-compose-training-mars-photos $ git checkout repo-starter
Anda dapat menjelajahi kode di repositori GitHub Mars Photos
.
2. Memisahkan lapisan UI dan Lapisan data
Mengapa lapisan yang berbeda?
Memisahkan kode menjadi beberapa lapisan akan membuat aplikasi Anda lebih skalabel, lebih andal, dan lebih mudah diuji. Adanya beberapa lapisan dengan penentuan batas yang jelas juga akan mempermudah beberapa developer untuk mengerjakan aplikasi yang sama tanpa saling memberikan dampak negatif.
Arsitektur aplikasi yang direkomendasikan Android menyatakan bahwa aplikasi minimal harus memiliki lapisan UI dan lapisan data.
Dalam codelab ini, Anda akan berfokus pada lapisan data dan melakukan perubahan agar aplikasi Anda mengikuti praktik terbaik yang direkomendasikan.
Apa yang dimaksud dengan lapisan data?
Lapisan data bertanggung jawab atas logika bisnis aplikasi serta pencarian dan penyimpanan data aplikasi. Lapisan data mengekspos data ke lapisan UI menggunakan pola Aliran Data Searah. Data dapat berasal dari beberapa sumber, seperti permintaan jaringan, database lokal, atau dari file di perangkat.
Aplikasi bahkan mungkin memiliki lebih dari satu sumber data. Saat dibuka, aplikasi akan mengambil data dari database lokal di perangkat yang merupakan sumber pertama. Saat berjalan, aplikasi akan membuat permintaan jaringan ke sumber kedua untuk mengambil data yang lebih baru.
Dengan menempatkan data di lapisan terpisah dari kode UI, Anda dapat membuat perubahan di satu bagian kode tanpa memengaruhi yang lainnya. Pendekatan ini merupakan bagian dari prinsip desain yang disebut pemisahan fokus. Bagian kode berfokus pada perhatiannya sendiri dan mengenkapsulasi cara kerja internalnya dari kode lain. Enkapsulasi adalah bentuk menyembunyikan cara kerja kode secara internal dari bagian kode lainnya. Jika satu bagian kode harus berinteraksi dengan bagian kode lain, interaksi ini akan dilakukan melalui antarmuka.
Fokus dari lapisan UI adalah menampilkan data yang disediakan. UI berhenti mengambil data karena tugas ini adalah fokus dari lapisan data.
Lapisan data terdiri dari satu atau beberapa repositori. Repositori dapat berisi nol atau beberapa sumber data.
Praktik terbaik mengharuskan aplikasi memiliki repositori untuk setiap jenis sumber data yang digunakan aplikasi Anda.
Dalam codelab ini, aplikasi memiliki satu sumber data dan akan memiliki satu repositori setelah Anda memfaktorkan ulang kode. Untuk aplikasi ini, repositori yang mengambil data dari internet akan menyelesaikan tanggung jawab sumber data. Proses ini dilakukan dengan membuat permintaan jaringan ke API. Jika coding sumber data lebih kompleks atau sumber data tambahan ditambahkan, tanggung jawab sumber data akan dienkapsulasi dalam class sumber data terpisah, dan repositori bertanggung jawab untuk mengelola semua sumber data.
Apa yang dimaksud dengan Repositori?
Berikut gambaran tugas class repositori secara umum:
- 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.
Aplikasi Mars Photos memiliki satu sumber data yang merupakan panggilan API jaringan. Aplikasi Mars Photos tidak memiliki logika bisnis karena hanya mengambil data. Data diekspos ke aplikasi melalui class repositori yang memisahkan sumber data.
3. Membuat Lapisan data
Pertama, Anda harus membuat class repositori. Panduan developer Android menyatakan bahwa class repositori diberi nama berdasarkan data yang menjadi tanggung jawabnya. Konvensi penamaan repositori adalah jenis data + Repositori. Di aplikasi Anda, namanya adalah MarsPhotosRepository
.
Membuat repositori
- Klik kanan com.example.marsphotos lalu pilih New > Package.
- Masukkan
data
ke dalam dialog. - Klik kanan pada paket
data
lalu pilih New > Kotlin Class/File. - Dalam dialog, pilih Interface, lalu masukkan
MarsPhotosRepository
sebagai nama antarmuka. - Di dalam antarmuka
MarsPhotosRepository
, tambahkan fungsi abstrak bernamagetMarsPhotos()
yang menampilkan daftar objekMarsPhoto
. Daftar ini dipanggil dari coroutine sehingga harus dideklarasikan dengansuspend
.
import com.example.marsphotos.model.MarsPhoto
interface MarsPhotosRepository {
suspend fun getMarsPhotos(): List<MarsPhoto>
}
- Di bawah deklarasi antarmuka, buat class bernama
NetworkMarsPhotosRepository
untuk mengimplementasikan antarmukaMarsPhotosRepository
. - Tambahkan antarmuka
MarsPhotosRepository
ke deklarasi class.
Pesan error muncul karena Anda tidak mengganti metode abstrak antarmuka. Langkah berikutnya akan mengatasi error ini.
- Di dalam class
NetworkMarsPhotosRepository
, ganti fungsi abstrakgetMarsPhotos()
. Fungsi ini menampilkan data dari pemanggilanMarsApi.retrofitService.getPhotos()
.
import com.example.marsphotos.network.MarsApi
class NetworkMarsPhotosRepository() : MarsPhotosRepository {
override suspend fun getMarsPhotos(): List<MarsPhoto> {
return MarsApi.retrofitService.getPhotos()
}
}
Selanjutnya, Anda harus memperbarui kode ViewModel
untuk menggunakan repositori guna mendapatkan data seperti yang disarankan oleh praktik terbaik Android.
- Buka file
ui/screens/MarsViewModel.kt
. - Scroll ke bawah, ke metode
getMarsPhotos()
. - Ganti baris "
val listResult = MarsApi.retrofitService.getPhotos()
" dengan kode berikut:
import com.example.marsphotos.data.NetworkMarsPhotosRepository
val marsPhotosRepository = NetworkMarsPhotosRepository()
val listResult = marsPhotosRepository.getMarsPhotos()
- Jalankan aplikasi. Perhatikan bahwa hasil yang ditampilkan sama dengan hasil sebelumnya.
Bukan ViewModel
yang langsung membuat permintaan jaringan untuk data, melainkan repositori yang menyediakan data. ViewModel
tidak lagi merujuk langsung ke kode MarsApi
.
Pendekatan ini membantu membuat kode yang mengambil data dikaitkan secara longgar dari ViewModel
. Dikaitkan secara longgar memungkinkan perubahan dilakukan di ViewModel
atau repositori tanpa berpengaruh terhadap yang lain, selama repositori memiliki fungsi yang disebut getMarsPhotos()
.
Kini kita dapat membuat perubahan pada implementasi di dalam repositori tanpa memengaruhi pemanggil. Untuk aplikasi yang lebih besar, perubahan ini dapat mendukung beberapa pemanggil.
4. Injeksi dependensi
Sering kali, class memerlukan objek dari class lain agar dapat berfungsi. Jika class memerlukan class lain, class yang diperlukan disebut dependensi.
Dalam contoh berikut, objek Car
bergantung pada objek Engine
.
Class memiliki dua cara untuk mendapatkan objek yang diperlukan ini. Salah satu caranya adalah dengan memerintahkan class membuat instance objek yang diperlukan itu sendiri.
interface Engine {
fun start()
}
class GasEngine : Engine {
override fun start() {
println("GasEngine started!")
}
}
class Car {
private val engine = GasEngine()
fun start() {
engine.start()
}
}
fun main() {
val car = Car()
car.start()
}
Cara lainnya adalah dengan meneruskan objek yang diperlukan sebagai argumen.
interface Engine {
fun start()
}
class GasEngine : Engine {
override fun start() {
println("GasEngine started!")
}
}
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main() {
val engine = GasEngine()
val car = Car(engine)
car.start()
}
Tidaklah sulit untuk membuat instance objek yang diperlukan, tetapi pendekatan ini menjadikan kode tidak fleksibel dan lebih sulit diuji karena class dan objek yang diperlukan dikaitkan dengan erat.
Class panggilan harus memanggil konstruktor objek yang merupakan detail implementasi. Jika konstruktor berubah, kode panggilan juga harus diubah.
Agar kode lebih fleksibel dan mudah disesuaikan, class tidak boleh membuat instance objek tempatnya bergantung. Instance untuk objek yang menjadi tempat class bergantung harus dibuat di luar class, lalu diteruskan. Pendekatan ini menghasilkan kode yang lebih fleksibel karena class tidak lagi di-hardcode ke satu objek tertentu. Implementasi objek yang diperlukan dapat berubah tanpa harus mengubah kode panggilan.
Melanjutkan contoh sebelumnya, ElectricEngine
dapat dibuat dan diteruskan ke class Car
jika diperlukan. Class Car
tidak perlu diubah dengan cara apa pun.
interface Engine {
fun start()
}
class ElectricEngine : Engine {
override fun start() {
println("ElectricEngine started!")
}
}
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main() {
val engine = ElectricEngine()
val car = Car(engine)
car.start()
}
Meneruskan objek yang diperlukan disebut injeksi dependensi (DI). Tindakan ini juga dikenal sebagai inversi kontrol.
DI adalah kondisi ketika dependensi disediakan saat runtime, bukan di-hardcode ke dalam class panggilan.
Mengimplementasikan injeksi dependensi:
- Membantu penggunaan kembali kode. Kode tidak bergantung pada objek tertentu sehingga dapat memberikan fleksibilitas yang lebih besar.
- Memudahkan pemfaktoran ulang. Pemfaktoran ulang satu bagian kode tidak memengaruhi bagian kode lainnya karena kode dikaitkan secara longgar.
- Membantu pengujian. Objek pengujian dapat diteruskan selama pengujian.
Salah satu contoh cara DI dapat membantu pengujian adalah saat menguji kode panggilan jaringan. Untuk pengujian ini, Anda mencoba menguji bahwa panggilan jaringan dilakukan dan data ditampilkan. Jika harus membayar setiap kali membuat permintaan jaringan selama pengujian, Anda mungkin memutuskan untuk tidak menguji kode ini karena mahal. Sekarang, bayangkan jika kita dapat memalsukan permintaan jaringan untuk pengujian. Seberapa bahagianya (dan lebih kayanya) Anda karenanya? Untuk pengujian, Anda dapat meneruskan objek pengujian ke repositori yang menampilkan data palsu saat dipanggil tanpa harus melakukan panggilan jaringan yang sebenarnya.
Kami ingin membuat ViewModel
dapat diuji, tetapi untuk saat ini kondisi ini bergantung pada repositori yang melakukan panggilan jaringan yang sebenarnya. Saat melakukan pengujian dengan repositori produksi yang sebenarnya, metode ini akan membuat banyak panggilan jaringan. Untuk memperbaiki masalah ini, daripada ViewModel
yang membuat repositori, kami memilih cara lain untuk memutuskan dan meneruskan instance repositori yang akan digunakan untuk produksi dan pengujian secara dinamis.
Proses ini dilakukan dengan mengimplementasikan penampung aplikasi yang menyediakan repositori ke MarsViewModel
.
Penampung adalah objek yang berisi dependensi yang diperlukan aplikasi. Dependensi ini digunakan di seluruh aplikasi sehingga harus berada di tempat umum yang dapat digunakan oleh semua aktivitas. Anda dapat membuat subclass dari class Aplikasi dan menyimpan referensi ke penampung.
Membuat Penampung Aplikasi
- Klik kanan pada paket
data
lalu pilih New > Kotlin Class/File. - Dalam dialog, pilih Interface, lalu masukkan
AppContainer
sebagai nama antarmuka. - Di dalam antarmuka
AppContainer
, tambahkan properti abstrak bernamamarsPhotosRepository
dari jenisMarsPhotosRepository
. - Di bawah definisi antarmuka, buat class dengan nama
DefaultAppContainer
yang menerapkan antarmukaAppContainer
. - Dari
network/MarsApiService.kt
, pindahkan kode untuk variabelBASE_URL
,retrofit
, danretrofitService
ke dalam classDefaultAppContainer
sehingga semuanya berada dalam penampung yang mempertahankan dependensi.
import retrofit2.Retrofit
import com.example.marsphotos.network.MarsApiService
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
class DefaultAppContainer : AppContainer {
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
private val retrofit: Retrofit = Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.baseUrl(BASE_URL)
.build()
private val retrofitService: MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
}
- Untuk variabel
BASE_URL
, hapus kata kunciconst
.const
harus dihapus karenaBASE_URL
tidak lagi menjadi variabel tingkat atas dan sekarang menjadi properti classDefaultAppContainer
. Faktorkan ulang menjadi camelcasebaseUrl
. - Untuk variabel
retrofitService
, tambahkan pengubah visibilitasprivate
. Pengubahprivate
ditambahkan karena variabelretrofitService
hanya digunakan di dalam class berdasarkan propertimarsPhotosRepository
, sehingga tidak harus dapat diakses di luar class. - Class
DefaultAppContainer
mengimplementasikan antarmukaAppContainer
sehingga propertimarsPhotosRepository
harus diganti. Setelah variabelretrofitService
, tambahkan kode berikut:
override val marsPhotosRepository: MarsPhotosRepository by lazy {
NetworkMarsPhotosRepository(retrofitService)
}
Class DefaultAppContainer
yang telah selesai akan terlihat seperti berikut:
class DefaultAppContainer : AppContainer {
private val baseUrl =
"https://android-kotlin-fun-mars-server.appspot.com"
/**
* Use the Retrofit builder to build a retrofit object using a kotlinx.serialization converter
*/
private val retrofit = Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.baseUrl(baseUrl)
.build()
private val retrofitService: MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
override val marsPhotosRepository: MarsPhotosRepository by lazy {
NetworkMarsPhotosRepository(retrofitService)
}
}
- Buka file
data/MarsPhotosRepository.kt
. Sekarang kita meneruskanretrofitService
keNetworkMarsPhotosRepository
, dan Anda harus mengubah classNetworkMarsPhotosRepository
. - Dalam deklarasi class
NetworkMarsPhotosRepository
, tambahkan parameter konstruktormarsApiService
seperti yang ditunjukkan dalam kode berikut.
import com.example.marsphotos.network.MarsApiService
class NetworkMarsPhotosRepository(
private val marsApiService: MarsApiService
) : MarsPhotosRepository {
- Di class
NetworkMarsPhotosRepository
, di fungsigetMarsPhotos()
, ubah pernyataan return untuk mengambil data darimarsApiService
.
override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}
- Hapus impor berikut dari file
MarsPhotosRepository.kt
.
// Remove
import com.example.marsphotos.network.MarsApi
Dari file network/MarsApiService.kt
, kita telah memindahkan semua kode dari objek. Sekarang kita dapat menghapus deklarasi objek yang tersisa karena tidak lagi diperlukan.
- Hapus kode berikut:
object MarsApi {
}
5. Memasang penampung aplikasi ke aplikasi
Langkah-langkah di bagian ini menghubungkan objek aplikasi ke penampung aplikasi seperti yang ditampilkan dalam gambar berikut.
- Klik kanan
com.example.marsphotos
, lalu pilih New > Kotlin Class/File. - Masukkan
MarsPhotosApplication
ke dalam dialog. Class ini mewarisi dari objek aplikasi sehingga Anda perlu menambahkannya ke deklarasi class.
import android.app.Application
class MarsPhotosApplication : Application() {
}
- Di dalam class
MarsPhotosApplication
, deklarasikan variabel yang disebutcontainer
dari jenisAppContainer
untuk menyimpan objekDefaultAppContainer
. Variabel diinisialisasi selama panggilan keonCreate()
, sehingga variabel harus ditandai dengan pengubahlateinit
.
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
- File
MarsPhotosApplication.kt
lengkap akan terlihat seperti kode berikut:
package com.example.marsphotos
import android.app.Application
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer
class MarsPhotosApplication : Application() {
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer()
}
}
- Anda harus memperbarui manifes Android agar aplikasi dapat menggunakan class aplikasi yang baru saja Anda tentukan. Buka file
manifests/AndroidManifest.xml
.
- Di bagian
application
, tambahkan atributandroid:name
dengan nilai nama class aplikasi".MarsPhotosApplication"
.
<application
android:name=".MarsPhotosApplication"
android:allowBackup="true"
...
</application>
6. Menambahkan repositori ke ViewModel
Setelah Anda menyelesaikan langkah-langkah ini, ViewModel
dapat memanggil objek repositori untuk mengambil data Mars.
- Buka file
ui/screens/MarsViewModel.kt
. - Pada deklarasi class untuk
MarsViewModel
, tambahkan parameter konstruktor pribadimarsPhotosRepository
dari jenisMarsPhotosRepository
. Nilai untuk parameter konstruktor berasal dari penampung aplikasi karena sekarang aplikasi menggunakan injeksi dependensi.
import com.example.marsphotos.data.MarsPhotosRepository
class MarsViewModel(private val marsPhotosRepository: MarsPhotosRepository) : ViewModel(){
- Dalam fungsi
getMarsPhotos()
, hapus baris kode berikut karenamarsPhotosRepository
kini diisi dalam panggilan konstruktor.
val marsPhotosRepository = NetworkMarsPhotosRepository()
- Framework Android tidak mengizinkan nilai
ViewModel
diteruskan dalam konstruktor saat dibuat. Oleh karena itu, kita akan mengimplementasikan objekViewModelProvider.Factory
yang dapat membantu mengatasi batasan ini.
Pola factory adalah pola pembuatan yang digunakan untuk membuat objek. Objek MarsViewModel.Factory
menggunakan penampung aplikasi untuk mengambil marsPhotosRepository
, lalu meneruskan repositori ini ke ViewModel
saat objek ViewModel
dibuat.
- Di bawah fungsi
getMarsPhotos()
, ketik kode untuk objek pendamping.
Objek pendamping membantu kita dengan menempatkan satu instance objek yang digunakan oleh semua orang tanpa harus membuat instance baru dari objek yang mahal. Ini adalah detail implementasi dan, dengan memisahkannya, kita dapat melakukan perubahan tanpa memengaruhi bagian lain dari kode aplikasi.
APPLICATION_KEY
adalah bagian dari objek ViewModelProvider.AndroidViewModelFactory.Companion
dan digunakan untuk menemukan objek MarsPhotosApplication
aplikasi yang memiliki properti container
yang digunakan untuk mengambil repositori yang digunakan untuk injeksi dependensi.
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.marsphotos.MarsPhotosApplication
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as MarsPhotosApplication)
val marsPhotosRepository = application.container.marsPhotosRepository
MarsViewModel(marsPhotosRepository = marsPhotosRepository)
}
}
}
- Buka file
theme/MarsPhotosApp.kt
, di dalam fungsiMarsPhotosApp()
, perbaruiviewModel()
untuk menggunakan factory.
Surface(
// ...
) {
val marsViewModel: MarsViewModel =
viewModel(factory = MarsViewModel.Factory)
// ...
}
Variabel marsViewModel
ini diisi oleh panggilan ke fungsi viewModel()
yang diteruskan MarsViewModel.Factory
dari objek pendamping sebagai argumen untuk membuat ViewModel
.
- Jalankan aplikasi untuk mengonfirmasi bahwa aplikasi masih berperilaku seperti sebelumnya.
Selamat, Anda telah memfaktorkan ulang aplikasi Mars Photos sehingga aplikasi ini dapat menggunakan repositori dan injeksi dependensi. Dengan mengimplementasikan lapisan data dengan repositori, UI dan kode sumber data telah dipisahkan agar mematuhi praktik terbaik Android.
Penggunaan injeksi dependensi akan mempermudah pengujian ViewModel
. Sekarang aplikasi Anda sudah lebih fleksibel, andal, dan siap untuk diskalakan.
Setelah melakukan peningkatan ini, kini saatnya untuk mempelajari cara mengujinya. Pengujian membuat kode Anda berperilaku seperti yang diharapkan dan mengurangi kemungkinan munculnya bug saat Anda terus mengerjakan kode.
7. Menyiapkan pengujian lokal
Di bagian sebelumnya, Anda telah mengimplementasikan repositori untuk memisahkan interaksi langsung dengan layanan REST API dari ViewModel
. Dengan praktik ini, Anda dapat menguji potongan-potongan kecil kode yang memiliki tujuan terbatas. Pengujian untuk potongan-potongan kecil kode dengan fungsi terbatas lebih mudah dibuat, diterapkan, dan dipahami daripada pengujian yang ditulis untuk potongan-potongan kode besar yang memiliki beberapa fungsi.
Anda juga telah mengimplementasikan repositori dengan memanfaatkan antarmuka, pewarisan, dan injeksi dependensi. Di bagian selanjutnya, Anda akan mempelajari alasan praktik terbaik arsitektur ini dapat mempermudah pengujian. Selain itu, Anda telah menggunakan coroutine Kotlin untuk membuat permintaan jaringan. Pengujian kode yang menggunakan coroutine memerlukan langkah tambahan untuk memperhitungkan eksekusi kode asinkron. Langkah-langkah ini akan dibahas nanti dalam codelab ini.
Menambahkan dependensi pengujian lokal
Tambahkan dependensi berikut ke app/build.gradle.kts
.
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
Membuat direktori pengujian lokal
- Buat direktori pengujian lokal dengan mengklik kanan direktori src dalam tampilan project, lalu memilih New > Directory > test/java.
- Buat paket baru di direktori pengujian yang diberi nama
com.example.marsphotos
.
8. Membuat data palsu dan dependensi untuk pengujian
Di bagian ini, Anda akan mempelajari cara injeksi dependensi membantu Anda menulis pengujian lokal. Sebelumnya di codelab, Anda telah membuat repositori yang bergantung pada layanan API. Selanjutnya Anda akan memodifikasi ViewModel
untuk bergantung pada repositori.
Setiap pengujian lokal hanya perlu menguji satu hal. Misalnya, ketika menguji fungsi model tampilan, Anda tidak ingin menguji fungsi repositori atau layanan API. Demikian pula, saat menguji repositori, Anda tidak ingin menguji layanan API.
Dengan menggunakan antarmuka dan kemudian injeksi dependensi untuk menyertakan class yang diwarisi dari antarmuka tersebut, Anda dapat menyimulasikan fungsi dependensi tersebut menggunakan class palsu yang dibuat hanya untuk tujuan pengujian. Dengan memasukkan class dan sumber data palsu untuk pengujian, kode dapat diuji secara terpisah dengan pengulangan dan konsistensi.
Hal pertama yang Anda perlukan adalah data palsu yang dapat digunakan di class palsu yang dibuat di lain waktu.
- Dalam direktori pengujian, buat paket dalam
com.example.marsphotos
yang disebutfake
. - Buat objek Kotlin baru di direktori
fake
yang diberi namaFakeDataSource
. - Di objek ini, buat properti yang ditetapkan ke daftar objek
MarsPhoto
. Daftar tidak harus panjang, tetapi harus berisi minimal dua objek.
object FakeDataSource {
const val idOne = "img1"
const val idTwo = "img2"
const val imgOne = "url.1"
const val imgTwo = "url.2"
val photosList = listOf(
MarsPhoto(
id = idOne,
imgSrc = imgOne
),
MarsPhoto(
id = idTwo,
imgSrc = imgTwo
)
)
}
Sebelumnya telah disebutkan dalam codelab ini bahwa repositori bergantung pada layanan API. Untuk membuat pengujian repositori, harus ada layanan API palsu yang menampilkan data palsu yang baru saja Anda buat. Jika layanan API palsu ini diteruskan ke repositori, repositori akan menerima data palsu saat metode dalam layanan API palsu dipanggil.
- Pada paket
fake
, buat class baru bernamaFakeMarsApiService
. - Siapkan class
FakeMarsApiService
untuk mewarisi dari antarmukaMarsApiService
.
class FakeMarsApiService : MarsApiService {
}
- Ganti fungsi
getPhotos()
.
override suspend fun getPhotos(): List<MarsPhoto> {
}
- Tampilkan daftar foto palsu dari metode
getPhotos()
.
override suspend fun getPhotos(): List<MarsPhoto> {
return FakeDataSource.photosList
}
Ingat, Anda tidak perlu khawatir jika masih belum memahami tujuan class ini. Penggunaan class palsu ini akan dijelaskan secara lebih mendetail di bagian berikutnya.
9. Menulis pengujian repositori
Di bagian ini, Anda akan menguji metode getMarsPhotos()
dari class NetworkMarsPhotosRepository
. Bagian ini menjelaskan penggunaan class palsu dan menunjukkan cara menguji coroutine.
- Di direktori palsu, buat class baru bernama
NetworkMarsRepositoryTest
. - Buat metode baru di class bernama
networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList()
yang baru saja Anda buat dan anotasikan dengan@Test
.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
}
Untuk menguji repositori, Anda memerlukan instance NetworkMarsPhotosRepository
. Ingat bahwa class ini bergantung pada antarmuka MarsApiService
. Di sinilah layanan API palsu dari bagian sebelumnya dimanfaatkan.
- Buat instance
NetworkMarsPhotosRepository
lalu teruskanFakeMarsApiService
sebagai parametermarsApiService
.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
val repository = NetworkMarsPhotosRepository(
marsApiService = FakeMarsApiService()
)
}
Dengan meneruskan layanan API palsu, panggilan apa pun ke properti marsApiService
dalam repositori akan menghasilkan panggilan ke FakeMarsApiService
. Dengan meneruskan class palsu untuk dependensi, Anda dapat dengan tepat mengontrol item yang ditampilkan dependensi tersebut. Pendekatan ini memastikan bahwa kode yang Anda uji tidak bergantung pada kode yang belum diuji atau API yang dapat mengubah atau memiliki masalah yang tidak terprediksi. Situasi tersebut dapat menyebabkan pengujian gagal meskipun kode yang Anda tulis sudah benar. Produk palsu membantu menciptakan lingkungan pengujian yang lebih konsisten, mengurangi kegagalan pengujian, dan memfasilitasi pengujian ringkas yang menguji satu fungsi.
- Nyatakan bahwa data yang ditampilkan oleh metode
getMarsPhotos()
sama denganFakeDataSource.photosList
.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList(){
val repository = NetworkMarsPhotosRepository(
marsApiService = FakeMarsApiService()
)assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
}
Perhatikan bahwa di IDE Anda, panggilan metode getMarsPhotos()
digarisbawahi dengan warna merah.
Jika mengarahkan mouse ke metode tersebut, Anda dapat melihat tooltip yang menunjukkan bahwa "Suspend function ‘getMarsPhotos' should be called only from a coroutine or another suspend function:"
Dalam data/MarsPhotosRepository.kt
, dengan melihat implementasi getMarsPhotos()
di NetworkMarsPhotosRepository
, Anda melihat bahwa fungsi getMarsPhotos()
merupakan fungsi penangguhan.
class NetworkMarsPhotosRepository(
private val marsApiService: MarsApiService
) : MarsPhotosRepository {
/** Fetches list of MarsPhoto from marsApi*/
override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}
Ingat, dengan memanggil fungsi ini dari MarsViewModel
, berarti Anda memanggil metode ini dari coroutine dengan memanggilnya dari lambda yang diteruskan ke viewModelScope.launch()
. Anda juga harus memanggil fungsi penangguhan, seperti getMarsPhotos()
, dari coroutine dalam pengujian. Namun, pendekatannya berbeda. Bagian berikutnya akan membahas cara menyelesaikan masalah ini.
Menguji coroutine
Di bagian ini, Anda akan mengubah pengujian networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList()
sehingga isi metode pengujian dijalankan dari coroutine.
- Ubah fungsi
networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList()
menjadi ekspresi dalamNetworkMarsRepositoryTest.kt
.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
- Tetapkan ekspresi yang sama dengan fungsi
runTest()
. Metode ini meminta lambda.
...
import kotlinx.coroutines.test.runTest
...
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
runTest {}
Library pengujian coroutine menyediakan fungsi runTest()
. Fungsi ini memanfaatkan metode yang Anda teruskan di lambda dan menjalankannya dari TestScope
yang diwarisi dari CoroutineScope
.
- Pindahkan konten fungsi pengujian ke fungsi lambda.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
runTest {
val repository = NetworkMarsPhotosRepository(
marsApiService = FakeMarsApiService()
)
assertEquals(FakeDataSource.photosList, repository.getMarsPhotos())
}
Perhatikan bahwa garis merah di bawah getMarsPhotos()
kini telah hilang. Jika pengujian dapat dijalankan, ini artinya Anda lulus.
10. Menulis pengujian ViewModel
Di bagian ini, Anda akan menulis pengujian untuk fungsi getMarsPhotos()
dari MarsViewModel
. MarsViewModel
bergantung pada MarsPhotosRepository
. Oleh karena itu, Anda harus membuat MarsPhotosRepository
palsu untuk bisa menulis pengujian ini. Selain itu, ada beberapa langkah tambahan yang harus dipertimbangkan untuk coroutine selain penggunaan metode runTest()
.
Membuat repositori palsu
Tujuan dari langkah ini adalah membuat class palsu yang diwarisi dari antarmuka MarsPhotosRepository
dan mengganti fungsi getMarsPhotos()
untuk menampilkan data palsu. Pendekatan ini mirip dengan pendekatan yang Anda lakukan dengan layanan API palsu. Perbedaannya adalah class ini memperluas antarmuka MarsPhotosRepository
, bukan MarsApiService
.
- Buat class baru di direktori
fake
yang diberi namaFakeNetworkMarsPhotosRepository
. - Perluas class ini dengan antarmuka
MarsPhotosRepository
.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
}
- Ganti fungsi
getMarsPhotos()
.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
override suspend fun getMarsPhotos(): List<MarsPhoto> {
}
}
- Tampilkan
FakeDataSource.photosList
dari fungsigetMarsPhotos()
.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
override suspend fun getMarsPhotos(): List<MarsPhoto> {
return FakeDataSource.photosList
}
}
Menulis pengujian ViewModel
- Buat class baru bernama
MarsViewModelTest
. - Buat fungsi dengan nama
marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
dan anotasikan dengan@Test
.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
- Jadikan fungsi ini sebagai ekspresi yang ditetapkan ke hasil metode
runTest()
untuk memastikan bahwa pengujian dijalankan dari coroutine, seperti pengujian repositori di bagian sebelumnya.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest{
}
- Dalam isi lambda
runTest()
, buat instanceMarsViewModel
lalu teruskan instance repositori palsu yang Anda buat.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest{
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
}
- Nyatakan bahwa
marsUiState
dari instanceViewModel
Anda cocok dengan hasil panggilan yang berhasil keMarsPhotosRepository.getMarsPhotos()
.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest {
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
assertEquals(
MarsUiState.Success("Success: ${FakeDataSource.photosList.size} Mars " +
"photos retrieved"),
marsViewModel.marsUiState
)
}
Pengujian akan gagal jika Anda mencoba menjalankannya apa adanya. Error tersebut terlihat seperti contoh berikut:
Exception in thread "Test worker @coroutine#1" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
Ingat bahwa MarsViewModel
memanggil repositori menggunakan viewModelScope.launch()
. Petunjuk ini akan meluncurkan coroutine baru pada dispatcher coroutine default yang disebut dispatcher Main
. Dispatcher Main
menggabungkan UI thread Android. Alasan error sebelumnya ialah UI thread Android tidak tersedia dalam pengujian unit. Pengujian unit dijalankan di workstation Anda, bukan di perangkat Android atau Emulator. Jika kode dalam pengujian unit lokal merujuk pada dispatcher Main
, pengecualian (seperti di atas) akan ditampilkan saat pengujian unit dijalankan. Untuk mengatasi masalah ini, Anda harus secara eksplisit menentukan dispatcher default saat menjalankan pengujian unit. Buka bagian berikutnya untuk mempelajari cara melakukannya.
Membuat dispatcher pengujian
Dispatcher Main
hanya tersedia dalam konteks UI. Oleh karena itu, Anda harus menggantinya dengan dispatcher yang cocok untuk pengujian unit. Library Coroutine Kotlin menyediakan dispatcher coroutine untuk tujuan ini dengan nama TestDispatcher
. TestDispatcher
harus digunakan, bukan dispatcher Main
untuk pengujian unit apa pun tempat coroutine baru dibuat, seperti halnya dengan fungsi getMarsPhotos()
dari model tampilan.
Gunakan fungsi Dispatchers.setMain()
untuk mengganti dispatcher Main
dengan TestDispatcher
di semua kasus. Anda dapat menggunakan fungsi Dispatchers.resetMain()
untuk mereset dispatcher thread kembali ke dispatcher Main
. Untuk menghindari duplikasi kode yang menggantikan dispatcher Main
dalam setiap pengujian, Anda dapat mengekstraknya ke dalam aturan pengujian JUnit. TestRule memberikan cara untuk mengontrol lingkungan tempat pengujian dijalankan. TestRule dapat menambahkan pemeriksaan tambahan, melakukan penyiapan atau pembersihan yang diperlukan untuk pengujian, atau mungkin mengamati eksekusi uji untuk melaporkannya di tempat lain. Tugas dapat dengan mudah dibagikan di antara kelas pengujian.
Buat class khusus untuk menulis TestRule guna mengganti dispatcher Main
. Untuk menerapkan TestRule kustom, selesaikan langkah-langkah berikut:
- Buat paket baru dalam direktori pengujian yang diberi nama
rules
. - Di direktori aturan, buat class baru bernama
TestDispatcherRule
. - Perluas
TestDispatcherRule
denganTestWatcher
. ClassTestWatcher
memungkinkan Anda mengambil tindakan di berbagai fase eksekusi pengujian.
class TestDispatcherRule(): TestWatcher(){
}
- Buat parameter konstruktor
TestDispatcher
untukTestDispatcherRule
.
Parameter ini memungkinkan penggunaan berbagai dispatcher, seperti StandardTestDispatcher
. Parameter konstruktor ini harus memiliki nilai default yang ditetapkan ke instance objek UnconfinedTestDispatcher
. Class UnconfinedTestDispatcher
mewarisi dari class TestDispatcher
dan menentukan bahwa tugas tidak boleh dijalankan dalam urutan tertentu. Pola eksekusi ini bagus untuk pengujian sederhana karena coroutine ditangani secara otomatis. Tidak seperti UnconfinedTestDispatcher
, class StandardTestDispatcher
dapat sepenuhnya mengontrol eksekusi coroutine. Cara ini lebih disarankan untuk pengujian rumit yang memerlukan pendekatan manual, tetapi tidak diperlukan untuk pengujian dalam codelab ini.
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
}
- Tujuan utama dari aturan pengujian ini adalah mengganti dispatcher
Main
dengan dispatcher pengujian sebelum pengujian mulai dijalankan. Fungsistarting()
dari classTestWatcher
akan dieksekusi sebelum pengujian tertentu dijalankan. Ganti fungsistarting()
.
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
}
}
- Tambahkan panggilan ke
Dispatchers.setMain()
, dengan meneruskantestDispatcher
sebagai argumen.
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
}
- Setelah eksekusi uji selesai, reset dispatcher
Main
dengan mengganti metodefinished()
. Panggil fungsiDispatchers.resetMain()
.
class TestDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
Aturan TestDispatcherRule
siap digunakan kembali.
- Buka file
MarsViewModelTest.kt
. - Di class
MarsViewModelTest
, buat instance classTestDispatcherRule
lalu tetapkan ke properti hanya bacatestDispatcher
.
class MarsViewModelTest {
val testDispatcher = TestDispatcherRule()
...
}
- Untuk menerapkan aturan ini ke pengujian, tambahkan anotasi
@get:Rule
ke propertitestDispatcher
.
class MarsViewModelTest {
@get:Rule
val testDispatcher = TestDispatcherRule()
...
}
- Jalankan kembali pengujian. Konfirmasikan bahwa pengujian kali ini berhasil.
11. Mendapatkan kode solusi
Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah berikut:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git $ cd basic-android-kotlin-compose-training-mars-photos $ git checkout coil-starter
Atau, Anda dapat mendownload repositori sebagai file ZIP, lalu mengekstraknya, dan membukanya di Android Studio.
Jika Anda ingin melihat kode solusi untuk codelab ini, lihat kode tersebut di GitHub.
12. Kesimpulan
Selamat, Anda telah menyelesaikan codelab ini dan memfaktorkan ulang aplikasi Mars Photos untuk menerapkan pola repositori dan injeksi dependensi.
Sekarang kode aplikasi sudah mengikuti praktik terbaik Android untuk lapisan data, yang berarti aplikasi tersebut lebih fleksibel, andal, dan mudah diskalakan.
Perubahan ini juga membantu mempermudah pengujian aplikasi. Manfaat ini sangat penting karena kode dapat terus berkembang sekaligus memastikan perilaku kode tersebut tetap seperti yang diharapkan.
Jangan lupa untuk membagikan karya Anda di media sosial dengan #AndroidBasics.
13. Mempelajari lebih lanjut
Dokumentasi developer Android:
Lainnya: