Menambahkan repositori dan DI Manual

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.

dbf927072d3070f0.png

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.

ff7a7cd039402747.png

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

  1. Klik kanan com.example.marsphotos lalu pilih New > Package.
  2. Masukkan data ke dalam dialog.
  3. Klik kanan pada paket data lalu pilih New > Kotlin Class/File.
  4. Dalam dialog, pilih Interface, lalu masukkan MarsPhotosRepository sebagai nama antarmuka.
  5. Di dalam antarmuka MarsPhotosRepository, tambahkan fungsi abstrak bernama getMarsPhotos() yang menampilkan daftar objek MarsPhoto. Daftar ini dipanggil dari coroutine sehingga harus dideklarasikan dengan suspend.
import com.example.marsphotos.model.MarsPhoto

interface MarsPhotosRepository {
    suspend fun getMarsPhotos(): List<MarsPhoto>
}
  1. Di bawah deklarasi antarmuka, buat class bernama NetworkMarsPhotosRepository untuk mengimplementasikan antarmuka MarsPhotosRepository.
  2. Tambahkan antarmuka MarsPhotosRepository ke deklarasi class.

Pesan error muncul karena Anda tidak mengganti metode abstrak antarmuka. Langkah berikutnya akan mengatasi error ini.

Screenshot Android Studio yang menampilkan antarmuka MarsPhotosRepository dan class NetworkMarsPhotosRepository

  1. Di dalam class NetworkMarsPhotosRepository, ganti fungsi abstrak getMarsPhotos(). Fungsi ini menampilkan data dari pemanggilan MarsApi.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.

  1. Buka file ui/screens/MarsViewModel.kt.
  2. Scroll ke bawah, ke metode getMarsPhotos().
  3. Ganti baris "val listResult = MarsApi.retrofitService.getPhotos()" dengan kode berikut:
import com.example.marsphotos.data.NetworkMarsPhotosRepository

val marsPhotosRepository = NetworkMarsPhotosRepository()
val listResult = marsPhotosRepository.getMarsPhotos()

5313985852c151aa.png

  1. 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. diagram alir untuk menunjukkan cara lapisan data diakses langsung dari Viewmodel sebelumnya. Sekarang kita memiliki repositori foto Mars

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. 1ea410d6670b7670.png

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

  1. Klik kanan pada paket data lalu pilih New > Kotlin Class/File.
  2. Dalam dialog, pilih Interface, lalu masukkan AppContainer sebagai nama antarmuka.
  3. Di dalam antarmuka AppContainer, tambahkan properti abstrak bernama marsPhotosRepository dari jenis MarsPhotosRepository. 7ed26c6dcf607a55.png
  4. Di bawah definisi antarmuka, buat class dengan nama DefaultAppContainer yang menerapkan antarmuka AppContainer.
  5. Dari network/MarsApiService.kt, pindahkan kode untuk variabel BASE_URL, retrofit, dan retrofitService ke dalam class DefaultAppContainer 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)
    }

}
  1. Untuk variabel BASE_URL, hapus kata kunci const. const harus dihapus karena BASE_URL tidak lagi menjadi variabel tingkat atas dan sekarang menjadi properti class DefaultAppContainer. Faktorkan ulang menjadi camelcase baseUrl.
  2. Untuk variabel retrofitService, tambahkan pengubah visibilitas private. Pengubah private ditambahkan karena variabel retrofitService hanya digunakan di dalam class berdasarkan properti marsPhotosRepository, sehingga tidak harus dapat diakses di luar class.
  3. Class DefaultAppContainer mengimplementasikan antarmuka AppContainer sehingga properti marsPhotosRepository harus diganti. Setelah variabel retrofitService, 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)
    }
}
  1. Buka file data/MarsPhotosRepository.kt. Sekarang kita meneruskan retrofitService ke NetworkMarsPhotosRepository, dan Anda harus mengubah class NetworkMarsPhotosRepository.
  2. Dalam deklarasi class NetworkMarsPhotosRepository, tambahkan parameter konstruktor marsApiService seperti yang ditunjukkan dalam kode berikut.
import com.example.marsphotos.network.MarsApiService

class NetworkMarsPhotosRepository(
    private val marsApiService: MarsApiService
) : MarsPhotosRepository {
  1. Di class NetworkMarsPhotosRepository, di fungsi getMarsPhotos(), ubah pernyataan return untuk mengambil data dari marsApiService.
override suspend fun getMarsPhotos(): List<MarsPhoto> = marsApiService.getPhotos()
}
  1. 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.

  1. 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.

92e7d7b79c4134f0.png

  1. Klik kanan com.example.marsphotos, lalu pilih New > Kotlin Class/File.
  2. 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() {
}
  1. Di dalam class MarsPhotosApplication, deklarasikan variabel yang disebut container dari jenis AppContainer untuk menyimpan objek DefaultAppContainer. Variabel diinisialisasi selama panggilan ke onCreate(), sehingga variabel harus ditandai dengan pengubah lateinit.
import com.example.marsphotos.data.AppContainer
import com.example.marsphotos.data.DefaultAppContainer

lateinit var container: AppContainer
override fun onCreate() {
    super.onCreate()
    container = DefaultAppContainer()
}
  1. 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()
    }
}
  1. Anda harus memperbarui manifes Android agar aplikasi dapat menggunakan class aplikasi yang baru saja Anda tentukan. Buka file manifests/AndroidManifest.xml.

759144e4e0634ed8.png

  1. Di bagian application, tambahkan atribut android: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.

7425864315cb5e6f.png

  1. Buka file ui/screens/MarsViewModel.kt.
  2. Pada deklarasi class untuk MarsViewModel, tambahkan parameter konstruktor pribadi marsPhotosRepository dari jenis MarsPhotosRepository. 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(){
  1. Dalam fungsi getMarsPhotos(), hapus baris kode berikut karena marsPhotosRepository kini diisi dalam panggilan konstruktor.
val marsPhotosRepository = NetworkMarsPhotosRepository()
  1. Framework Android tidak mengizinkan nilai ViewModel diteruskan dalam konstruktor saat dibuat. Oleh karena itu, kita akan mengimplementasikan objek ViewModelProvider.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.

  1. 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)
       }
   }
}
  1. Buka file theme/MarsPhotosApp.kt, di dalam fungsi MarsPhotosApp(), perbarui viewModel() 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.

  1. 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

  1. Buat direktori pengujian lokal dengan mengklik kanan direktori src dalam tampilan project, lalu memilih New > Directory > test/java.
  2. 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.

  1. Dalam direktori pengujian, buat paket dalam com.example.marsphotos yang disebut fake.
  2. Buat objek Kotlin baru di direktori fake yang diberi nama FakeDataSource.
  3. 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.

  1. Pada paket fake, buat class baru bernama FakeMarsApiService.
  2. Siapkan class FakeMarsApiService untuk mewarisi dari antarmuka MarsApiService.
class FakeMarsApiService : MarsApiService {
}
  1. Ganti fungsi getPhotos().
override suspend fun getPhotos(): List<MarsPhoto> {
}
  1. 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.

  1. Di direktori palsu, buat class baru bernama NetworkMarsRepositoryTest.
  2. 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.

  1. Buat instance NetworkMarsPhotosRepository lalu teruskan FakeMarsApiService sebagai parameter marsApiService.
@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.

  1. Nyatakan bahwa data yang ditampilkan oleh metode getMarsPhotos() sama dengan FakeDataSource.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.

2bd5f8999e0f3ec2.png

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:"

d2d3b6d770677ef6.png

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.

  1. Ubah fungsi networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() menjadi ekspresi dalam NetworkMarsRepositoryTest.kt.
@Test
fun networkMarsPhotosRepository_getMarsPhotos_verifyPhotoList() =
  1. 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.

  1. 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.

  1. Buat class baru di direktori fake yang diberi nama FakeNetworkMarsPhotosRepository.
  2. Perluas class ini dengan antarmuka MarsPhotosRepository.
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
}
  1. Ganti fungsi getMarsPhotos().
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
   }
}
  1. Tampilkan FakeDataSource.photosList dari fungsi getMarsPhotos().
class FakeNetworkMarsPhotosRepository : MarsPhotosRepository{
   override suspend fun getMarsPhotos(): List<MarsPhoto> {
       return FakeDataSource.photosList
   }
}

Menulis pengujian ViewModel

  1. Buat class baru bernama MarsViewModelTest.
  2. Buat fungsi dengan nama marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() dan anotasikan dengan @Test.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
  1. 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{
    }
  1. Dalam isi lambda runTest(), buat instance MarsViewModel lalu teruskan instance repositori palsu yang Anda buat.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
    runTest{
        val marsViewModel = MarsViewModel(
            marsPhotosRepository = FakeNetworkMarsPhotosRepository()
         )
    }
  1. Nyatakan bahwa marsUiState dari instance ViewModel Anda cocok dengan hasil panggilan yang berhasil ke MarsPhotosRepository.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:

  1. Buat paket baru dalam direktori pengujian yang diberi nama rules.
  2. Di direktori aturan, buat class baru bernama TestDispatcherRule.
  3. Perluas TestDispatcherRule dengan TestWatcher. Class TestWatcher memungkinkan Anda mengambil tindakan di berbagai fase eksekusi pengujian.
class TestDispatcherRule(): TestWatcher(){

}
  1. Buat parameter konstruktor TestDispatcher untuk TestDispatcherRule.

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() {

}
  1. Tujuan utama dari aturan pengujian ini adalah mengganti dispatcher Main dengan dispatcher pengujian sebelum pengujian mulai dijalankan. Fungsi starting() dari class TestWatcher akan dieksekusi sebelum pengujian tertentu dijalankan. Ganti fungsi starting().
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        
    }
}
  1. Tambahkan panggilan ke Dispatchers.setMain(), dengan meneruskan testDispatcher sebagai argumen.
class TestDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }
}
  1. Setelah eksekusi uji selesai, reset dispatcher Main dengan mengganti metode finished(). Panggil fungsi Dispatchers.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.

  1. Buka file MarsViewModelTest.kt.
  2. Di class MarsViewModelTest, buat instance class TestDispatcherRule lalu tetapkan ke properti hanya baca testDispatcher.
class MarsViewModelTest {
    
    val testDispatcher = TestDispatcherRule()
    ...
}
  1. Untuk menerapkan aturan ini ke pengujian, tambahkan anotasi @get:Rule ke properti testDispatcher.
class MarsViewModelTest {
    @get:Rule
    val testDispatcher = TestDispatcherRule()
    ...
}
  1. 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: