Anda di halaman 1dari 18

Android MVVM — Part 2 Repository Sumber Data

Kita
Membangun Aplikasi MVVM Dengan Kotlin, Coroutines dan Architecture Component

Satria Adi Putra Follow


Aug 31, 2019 · 7 min read

Photo by Samuel Zeller on Unsplash

Repository
Sebelumnya kita udah bahas tentang pola MVVM waktu bikin aplikasi Android. Seperti
yang udah kita bahas, View akan melakukan observasi terhadap data di ViewModel dan
memiliki tanggung jawab untuk update tampilan ketika ada perubahan data di
ViewModel. Artinya View memiliki ketergantungan terhadap ViewModel. Sedangkan
ViewModel akan menyimpan data dalam bentuk LiveData yang nantinya akan
diobservasi oleh View. Dari mana asal data tersebut? Yaps benar! Repository.

Repository bertanggung jawab untuk semua data yang akan digunakan di aplikasi. Mau
itu menyimpan data, melakukan update data, menghapus data atau mencari data
serahkan semuanya kepada Repository. Kemana Repository akan menyimpan data atau
dari mana Repository akan mencari data? Biasanya aplikasi Android melakukan
penyimpanan ke dua tempat. Pertama Remote Data Store atau web service yang dimiliki.
Remote Data Store biasanya berupa cloud server atau server sik. Yang kedua adalah
Local Data Store. Kebalikan dari Remote Data Store, Local Data Store menyimpan datanya
langsung di device Android itu sendiri baik menggunakan Realm atau SQLite.

Photo from Guide to app architecture | Android Developers

Kenapa Dua Data Store?


Banyak alasan kenapa banyak aplikasi masih menyimpan ke Local Data Store, walaupun
sebenarnya sudah tersimpan di Remote Data Store. Kalau diliat dari tujuan-pun kedua
Data Store tersebut udah jelas beda.

Salah satu tujuan dari Remote Data Store itu buat sinkronisasi data antar device. Jadi
kalau misal ada pengguna yang ganti device, dia cukup login pake akun yang sama
dengan device sebelumnya. Jadi data dari device sebelumnya masih bisa dipake di
device yang baru.

Kalau Local Data Store, biasanya dipake buat ningkation pengalaman pengguna saat
pake aplikasi. Mencari data pada SQLite local jauh lebih cepat dibanding melakukan
request http ke server. Belum lagi device harus terhubung ke internet.

Dengan memanfaatkan keduanya, kita bisa membuat aplikasi kita menjadi O ine First
Apps.

. . .

Yuk Bikin!
Halaman pertama dari aplikasi yang kita bangun akan menampilkan list dari pokemon
set yang tersedia. Tampilannya kurang lebih akan seperti ini.
Sebelum kita membangun repository, kita akan membangun model dari set pokemon
ini.

1 data class PokemonSet(


2 var name: String,
3 @SerializedName("logoUrl") var logo: String
4 ) {
5 data class PokemonSetResponse(
6 var sets: MutableList<PokemonSet>
7 )
8 }

PokemonSet.kt hosted with ❤ by GitHub view raw

Kita akan membangun repository untuk set pokemon ini.

1 class PokemonSetRepository(
2 private val setLocalDataStore: PokemonSetDataStore,
3 private val setRemoteDataStore: PokemonSetDataStore
4 ) {
5 suspend fun getSets(): MutableList<PokemonSet>? {
6 val cache = setLocalDataStore.getSets()
7 if (cache != null) return cache
8 val response = setRemoteDataStore.getSets()
9 setLocalDataStore.addAll(response)
10 return response
11 }
12 }

Kayak yang udah dijelasin sebelumnya, Repository akan bergantung pada dua buah
Data Store. Untuk mendapatkan set pokemon yang tersedia, pertama repository bakal
cari data dari Local Data Store dulu. Kalau datanya ketemu berarti datanya langsung
dikirim. Kalau nggak ketemu, Repository bakal cari data dari Remote Data Store. Abis
dapet datanya, Repository bakal nyimpen datanya ke Local Data Store.

PokemonSetDataStore adalah sebuah interface yang dide nisikan sebagai berikut.

1 interface PokemonSetDataStore {
2 suspend fun getSets(): MutableList<PokemonSet>?
3 suspend fun addAll(sets: MutableList<PokemonSet>?)
4 }

PokemonSetDataStore.kt hosted with ❤ by GitHub view raw

Setelah mende nisikan interface tersebut, kita harus mende nisikan implementasinya.
Biar bisa pake tur coroutine dari kotlin, kita bikin fungsinya jadi suspend function.
Karena Repository kita membutuhkan 2 buah Data Store, maka kita akan membuat 2
class DataStore baru.

1 class PokemonSetLocalDataStore : PokemonSetDataStore {


2 private var caches = mutableListOf<PokemonSet>()
3
4 override suspend fun getSets(): MutableList<PokemonSet>? =
5 if (caches.isNotEmpty()) caches else null
6
7 override suspend fun addAll(sets: MutableList<PokemonSet>?) {
8 sets?.let { caches = it }
9 }
10 }

PokemonSetLocalDataStore kt hosted with ❤ by GitHub view raw

Pertama kita buat PokemonSetLocalDataStore. Data Store ini akan menyimpan data dari
web service di variabel cache. Gimana kalo misalnya kita bikin object Repository baru
dan DataStore baru, berarti variabel cache nanti bakal kosong padahal kita udah
nyimpen datanya? Yaps benar, data cache yang seharusnya udah disimpan sebelumnya
berubah lagi menjadi list baru yang masih kosong.

Solusi dari skenario itu adalah dengan memastikan object repository yang kita bikin
cuma ada satu dan kita hanya melakukan set up objectnya hanya sekali. Untuk
memastikan object Repository yang kita bangun cuma ada satu adalah dengan
mengimplementasikan Singleton Pattern.

1 class PokemonSetRepository private constructor () {


2 private var setLocalDataStore: PokemonSetDataStore? = null
3 private var setRemoteDataStore: PokemonSetDataStore? = null
4
5 fun init(setLocalDataStore: PokemonSetDataStore, setRemoteDataStore: PokemonSetRemoteDataSto
6 this.setLocalDataStore = setLocalDataStore
7 this.setRemoteDataStore = setRemoteDataStore
8 }
9
10 suspend fun getSets(): MutableList<PokemonSet>? {
11 val cache = setLocalDataStore?.getSets()
12 if (cache != null) return cache
13 val response = setRemoteDataStore?.getSets()
14 setLocalDataStore?.addAll(response)
15 return response
16 }
17
18 companion object {
19 val instance by lazy { PokemonSetRepository() }
20 }
21 }

Selanjutnya kita buat class PokemonSetRemoteDataStore. Tugas dari class ini adalah
melakukan request http menggunakan library Retro t 2. Sebenarnya penamaan class ini
bisa saja menjadi PokemonSetRetro tDataStore. Bergantung selera dan code convention
yang digunakan dalam tim.

1 class PokemonSetRemoteDataStore(private val pokemonTcgService: PokemonTcgService) :


2 PokemonSetDataStore {
3 override suspend fun getSets(): MutableList<PokemonSet>? {
4 val response = pokemonTcgService.getSets()
5 if (response.isSuccessful) return response.body()?.sets
6
7 throw Exception("Terjadi kesalahan saat melakukan request data, status error ${response.
8 }
9
10 override suspend fun addAll(sets: MutableList<PokemonSet>?) {
11 TODO("not implemented") //To change body of created functions use File | Settings | File
12 }
13 }

Kalau nanti kita butuh sebuah Data Store baru yang dapat menyimpan data ke dalam
SQLite pake library Room. Kita nggak perlu ngubah class PokemonSetLocalDataStore,
kita cukup buat sebuah class baru misalnya PokemonSetRoomDataStore dan
mengimplementasikan fungsi yang dibutuhkan.

Untuk service Retro t yang digunakan, implementasinya sebagai berikut.

1 object RetrofitApp {
2 private const val BASE_URL = "https://api.pokemontcg.io/v1/"
3
4 private val client = Retrofit.Builder()
5 .baseUrl(BASE_URL)
6 .addConverterFactory(GsonConverterFactory.create())
7 .build()
8
9 val POKEMON_TCG_SERVICE: PokemonTcgService = client.create(PokemonTcgService::class.java)
10 }
11
12 interface PokemonTcgService {
13 @GET("cards")
14 suspend fun getCards(@Query("set") set: String): Response<PokemonCard.PokemonCardResponse>
15
16 @GET("sets")
17 suspend fun getSets(): Response<PokemonSet.PokemonSetResponse>
18 }

Buat class test untuk nguji repository yang kita bangun.

1 class PokemonSetRepositoryTest {
2 @Mock
3 var localDataStore: PokemonSetDataStore? = null
4
5 @Mock
6 var remoteDataStore: PokemonSetDataStore? = null
7
8 var pokemonSetRepository: PokemonSetRepository? = null
9
10 var pokemonSets = mutableListOf<PokemonSet>()
11
12 @Before
13 fun init() {
14 MockitoAnnotations.initMocks(this)
15 pokemonSetRepository = PokemonSetRepository.instance.apply {
16 init(localDataStore!!, remoteDataStore!!)
17 }
18 }
19
20 @Test
21 fun shouldNotGetPokemonsFromRemoteWhenLocalIsNotNull() {
22 runBlocking {
23 `when`(localDataStore?.getSets()).thenReturn(pokemonSets)
24 pokemonSetRepository?.getSets()
25
26 verify(remoteDataStore, never())?.getSets()
27 verify(localDataStore, never())?.addAll(pokemonSets)
28 }
29 }
30
31 @Test
32 fun shouldCallGetPokemonsFromRemoteAndSaveToLocalWhenLocalIsNull() {
33 runBlocking {
34 `when`(localDataStore?.getSets()).thenReturn(null)
35 `when`(remoteDataStore?.getSets()).thenReturn(pokemonSets)
36 pokemonSetRepository?.getSets()
37
38 verify(remoteDataStore, times(1))?.getSets()
39 verify(localDataStore, times(1))?.addAll(pokemonSets)
40 }
41 }
42
43 @Test
44 fun shouldThrowExceptionWhenRemoteThrowAnException() {
45 runBlocking {
46 `when`(localDataStore?.getSets()).thenReturn(null)
47 `when`(remoteDataStore?.getSets()).thenAnswer { throw Exception() }
48
49 try {
50 pokemonSetRepository?.getSets()
51 } catch (ex: Exception) {
52 }

Test yang kita buat bakal nguji apakah repository yang kita bangun bekerja sesuai
dengan konsep yang sudah dijelaskan atau belum.

Fungsi shouldNotGetPokemonsFromRemoteWhenLocalIsNotNull buat nguji apa


Repository bakal melakukan request http kalau data dari Local Data Store nggak null
atau malah langsung kirim data dari Local Data Store. Yang kita pengen adalah
repository langsung ngirim data dari Local Data Store dan nggak melakukan request http
sama sekali.

Fungsi kedua shouldCallGetPokemonsFromRemoteAndSaveToLocalWhenLocalIsNull


buat nguji apa Repository bakal melakukan request http kalau data dari Local Data Store
null, menyimpan data dari hasil request http ke Local Data Store dan kirim hasil datanya.

Fungsi yang terakhir buat mastiin kalau repository akan melempar exception ketika
terjadi kegagalan waktu melakukan request http.
Repository yang kita bangun sudah sesuai dengan yang kita pengen. Selanjutnya kita
tinggal bangun Repository buat mendapatkan kartu pokemon.
Kayak yang udah dilakuin sebelumnya, pertama kita de nisiin dulu model buat kartu
pokemon.

1 data class PokemonCard(


2 var name: String?,
3 @SerializedName("imageUrl") var image: String?,
4 var rarity: String?,
5 var series: String?
6 ) : Parcelable {
7 data class PokemonCardResponse(
8 var cards: MutableList<PokemonCard>
9 )
10 }

PokemonCard kt hosted with ❤ by GitHub view raw

Selanjutnya kita bakal bangun repository dari kartu pokemon dan memastikan
repository mengimplementasikan Singleton Pattern.

1 class PokemonCardRepository private constructor() {


2 private var pokemonCardLocalDataStore: PokemonCardDataStore? = null
3 private var pokemonCardRemoteDataStore: PokemonCardDataStore? = null
4
5 fun init(pokemonLocalCardDataStore: PokemonCardDataStore, pokemonRemoteCardDataStore: Pokemo
6 this.pokemonCardLocalDataStore = pokemonLocalCardDataStore
7 this.pokemonCardRemoteDataStore = pokemonRemoteCardDataStore
8 }
9
10 suspend fun getPokemons(set: String): MutableList<PokemonCard>? {
11 val cache = pokemonCardLocalDataStore?.getPokemons(set)
12 if (cache != null) return cache
13 val response = pokemonCardRemoteDataStore?.getPokemons(set)
14 pokemonCardLocalDataStore?.addAll(set, response)
15 return response
16 }
17
18 companion object {
19 val instance by lazy { PokemonCardRepository() }
20 }
21 }

Ada yang ganjel? Berasa dejavu? Kita coba bandingin class PokemonCardRepository
dengan PokemonSetReporitory.

1 class PokemonCardRepository private constructor() {


2 private var pokemonCardLocalDataStore: PokemonCardDataStore? = null
3 private var pokemonCardRemoteDataStore: PokemonCardDataStore? = null
4
5 fun init(pokemonLocalCardDataStore: PokemonCardDataStore, pokemonRemoteCardDataStore: Pokemo
6 this.pokemonCardLocalDataStore = pokemonLocalCardDataStore
7 this.pokemonCardRemoteDataStore = pokemonRemoteCardDataStore
8 }
9 }
10
11 class PokemonSetRepository private constructor() {
12 private var setLocalDataStore: PokemonSetDataStore? = null
13 private var setRemoteDataStore: PokemonSetDataStore? = null
14
15 fun init(setLocalDataStore: PokemonSetDataStore, setRemoteDataStore: PokemonSetDataStore) {
16 this.setLocalDataStore = setLocalDataStore
17 this.setRemoteDataStore = setRemoteDataStore
18 }
19 }

Ternyata bener, ada sesuatu yang ngulan dan sama persis di PokemonCardRepository
sama PokemonSetRepository. Kita bakal menggeneralisasi dua class ini jadi nggak akan
dejavu lagi.

1 abstract class BaseRepository<DataStore> {


2 protected var localDataStore: DataStore? = null
3 protected var remoteDataStore: DataStore? = null
4
5 fun init(localDataStore: DataStore, remoteDataStore: DataStore) {
6 this.localDataStore = localDataStore
7 this.remoteDataStore = remoteDataStore
8 }
9 }
10
11 class PokemonSetRepository private constructor() : BaseRepository<PokemonSetDataStore>() {
12 suspend fun getSets(): MutableList<PokemonSet>? {
13 val cache = localDataStore?.getSets()
14 if (cache != null) return cache
15 val response = remoteDataStore?.getSets()
16 localDataStore?.addAll(response)
17 return response
18 }
19
20 companion object {
21 val instance by lazy { PokemonSetRepository() }
22 }
23 }
24
25 class PokemonCardRepository private constructor() : BaseRepository<PokemonCardDataStore>() {
26 suspend fun getPokemons(set: String): MutableList<PokemonCard>? {
27 val cache = localDataStore?.getPokemons(set)
28 if (cache != null) return cache
29 val response = remoteDataStore?.getPokemons(set)
30 localDataStore?.addAll(set, response)
31 return response
32 }
33
34 companion object {
35 val instance by lazy { PokemonCardRepository() }
36 }

Untuk mastiin perubahan ini nggak bikin repository sebelumnya error, kita jalanin lagi
unit test yang udah dibuat.

Mantap, semuanya lancar. Oke berikutnya kita harus de nisikan


PokemonCardDataStore biar PokemonCardRepository nggak error.

1 interface PokemonCardDataStore {
2 suspend fun getPokemons(set: String): MutableList<PokemonCard>?
3 suspend fun addAll(set: String, pokemons: MutableList<PokemonCard>?)
4 }

PokemonCardDataStore.kt hosted with ❤ by GitHub view raw

Dejavu lagi kah? PokemonCardDataStore dan PokemonSetDataStore punya banyak


kesamaan. Mereka cuma punya 2 fungsi, yang pertama buat mendapatkan list dan yang
kedua untuk menyimpan list yang sudah didapatkan. Apa perlu digeneralisasi lagi?
Hmmm kayaknya nggak. Kenapa? Karena selain mereka butuh parameter yang beda,
mereka juga punya tanggung jawab yang beda.

PokemonCardDataStore bertanggung jawab buat kartu pokemon sedangkan


PokemonSetDataStore bertanggung jawab buat set pokemon. Kalau kita liat lagi ke
sebelumnya di PokemonSetRemoteDataStore, fungsi addAll tidak diimplementasikan
sama sekali. Jadi kalau misal kita generalisasi 2 interface tersebut menjadi 1 interface,
nanti kalau ada perubahan proses bisnis di kartu pokemon di mana kita bisa tambah
kartu favorit berarti di interface yang baru kita bakal nambah 1 fungsi addFavorite.
Kalau gitu PokemonSetLocalDataStore dan PokemonSetRemoteDataStore harus
mengimplementasikan fungsi addToFavorite tersebut padahal ga ada implementasi yang
jelas buat set pokemon.

1 interface DataStore<Model> {
2 suspend fun findAll(): MutableList<Model>?
3 suspend fun addAll(models: MutableList<Model>?)
4 suspend fun addToFavorite(model: Model?)
5 }
6
7 class PokemonSetLocalDataStore : DataStore<Nothing, PokemonSet> {
8 private var caches = mutableListOf<PokemonSet>()
9
10 override suspend fun findAll(nothing: Nothing): MutableList<PokemonSet>? =
11 if (caches.isNotEmpty()) caches else null
12
13 override suspend fun addAll(sets: MutableList<PokemonSet>?) {
14 sets?.let { caches = it }
15 }
16
17 override suspend fun addToFavorite(model: PokemonSet?) {
18 TODO("not implemented") //To change body of created functions use File | Settings | File
19 }
20 }
21
22 class PokemonSetRemoteDataStore(private val pokemonTcgService: PokemonTcgService) :
23 DataStore<Nothing, PokemonSet> {
24 override suspend fun findAll(input: Nothing): MutableList<PokemonSet>? {
25 val response = pokemonTcgService.getSets()
26 if (response.isSuccessful) return response.body()?.sets
27
28 throw Exception("Terjadi kesalahan saat melakukan request data, status error ${response.
29 }
30
31 override suspend fun addAll(sets: MutableList<PokemonSet>?) {
32 TODO("not implemented") //To change body of created functions use File | Settings | File
33 }
34
35 override suspend fun addToFavorite(model: PokemonSet?) {
36 TODO("not implemented") //To change body of created functions use File | Settings | File
37 }

Bukan best practice

Overriding fungsi yang tidak diperlukan bukan best practice. Jadi hindari code yang
kayak gitu ya.
“A class should have only one reason to change”
Robert C. Martin
Kita balik ke PokemonCardDataStore, sekarang kita implementasikan menjadi
PokemonLocalDataStore dan PokemonRemoteDataStore.

1 class PokemonCardLocalDataStore : PokemonCardDataStore {


2 private val caches = mutableMapOf<String, MutableList<PokemonCard>?>()
3
4 override suspend fun getPokemons(set: String): MutableList<PokemonCard>? =
5 if (caches.contains(set)) caches[set] else null
6
7 override suspend fun addAll(set: String, pokemons: MutableList<PokemonCard>?) {
8 caches[set] = pokemons
9 }
10 }
11
12 class PokemonCardRemoteDataStore(private val pokemonTcgService: PokemonTcgService) : PokemonCardD
13 override suspend fun getPokemons(set: String): MutableList<PokemonCard>? {
14 val response = pokemonTcgService.getCards(set)
15 if (response.isSuccessful) return response.body()?.cards
16
17 throw Exception("Terjadi kesalahan saat melakukan request data, status error ${response.
18 }
19
20 override suspend fun addAll(set: String, pokemons: MutableList<PokemonCard>?) {
21 }
22 }

Ada satu perbedaan dari PokemonCardLocalDataStore dan PokemonSetLocalDataStore.


Kalau sebelumnya kita simpan cache di MutableList, sekarang kita simpan di
MutableMap. Karena kartu pokemon yang ditampilkan bakal beda-beda tergantung dari
set yang diklik, makannya cache-nya disimpan dalam bentuk MutableMap. Kita bakal
cari kartu pokemon sesuai dengan set yang diklik, kalau misalnya ga ada kita bakal
request http buat dapet data sesuai set tersebut.

Terakhir kita harus test repository yang udah kita dibangun.

1 class PokemonCardRepositoryTest {
2 @Mock
3 var localCardDataStore: PokemonCardDataStore? = null
4
5 @Mock
6 var remoteCardDataStore: PokemonCardDataStore? = null
7
8 var pokemonCardRepository: PokemonCardRepository? = null
9
10 @Before
11 fun init() {
12 MockitoAnnotations.initMocks(this)
13 pokemonCardRepository = PokemonCardRepository.instance.apply {
14 init(localCardDataStore!!, remoteCardDataStore!!)
15 }
16 }
17
18 @Test
19 fun shouldNotGetPokemonsFromRemoteWhenLocalIsNotNull() {
20 runBlocking {
21 `when`(localCardDataStore?.getPokemons(anyString())).thenReturn(mutableListOf())
22 pokemonCardRepository?.getPokemons(anyString())
23
24 verify(remoteCardDataStore, never())?.getPokemons(anyString())
25 verify(localCardDataStore, never())?.addAll(anyString(), any())
26 }
27 }
28
29 @Test
30 fun shouldCallGetPokemonsFromRemoteAndSaveToLocalWhenLocalIsNull() {
31 runBlocking {
32 `when`(localCardDataStore?.getPokemons(anyString())).thenReturn(null)
33 `when`(remoteCardDataStore?.getPokemons(anyString())).thenReturn(any())
34 pokemonCardRepository?.getPokemons("Test set")
35
36 verify(remoteCardDataStore, times(1))?.getPokemons(anyString())
37 verify(localCardDataStore, times(1))?.addAll(anyString(), any())
38 }
39 }
40
41 @Test
42 fun shouldThrowExceptionWhenRemoteThrowAnException() {
43 runBlocking {
44 `when`(localCardDataStore?.getPokemons(anyString())).thenReturn(null)
45 `when`(remoteCardDataStore?.getPokemons(anyString())).thenAnswer { throw Exception()
46
47 try {
48 pokemonCardRepository?.getPokemons("Test set")
49 } catch (ex: Exception) {
50 }
51 }

Dan seperti sebelumnya, fungsi pertama untuk menguji kalau repository akan
melakukan request http atau nggak kalau data dari Local Data Store tidak null. Fungsi
kedua untuk menguji apakah Repository melakukan request http kalau Local Data Store
null. Terakhir untuk menguji kalau Repository akan melempar exception ketika terjadi
kesalahan saat melakukan request http.

. . .

Finally!

Photo by Alexandru Zdrobău on Unsplash

Repository kita sudah siap, yay! Cukup banyak yang udah kita bahas tentang Repository.
Kita udah mengimplementasikan dua repository dan juga membuat unit test untuk
menguji dua repository tersebut. Mungkin kalau kalian sadar, kita tidak menggunakan
komponen dari framework Android (Context, Activity, Fragment dll) sama sekali selama
kita mengimplementasikan dua repository ini. Ini hal yang bagus.

Waktu membangun sebuah komponen, usahakan biar nggak nyentuh langsung


komponen framework Android. Ini memudahkan kita untuk membuat unit test dari
komponen tersebut. Kalau emang terpaksa harus menggunakan komponen framework
Android, lakukan Dependency Inversion Principle seperti yang kita lakukan pada
PokemonSetDataStore dan PokemonCardDataStore.

. . .

Parts
Artikel ini akan dibagi ke dalam beberapa bagian. Karena saya rasa memahami tugas
dari setiap komponen jauh lebih penting daripada hanya sekedar tau bagaimana MVVM
diimplementasikan. Bagian-bagian tersebut adalah sebagai berikut:

1. Part 1 Pengenalan MVVM

2. Part 2 Repository Sumber Data Kita

3. Part 3 ViewModel Si Pintar

4. Part 4 View Yang Sangat Reaktif

Referensi

Guide to app architecture | Android Developers


This guide encompasses best practices and recommended architecture for
building robust, production-quality apps. This…
developer.android.com

Realm: Create reactive mobile apps in a fraction of the time


MongoDB Realm will combine Realm, the popular mobile database and data
synchronization technology, and MongoDB Stitch…
realm.io
Save data using SQLite | Android Developers
Saving data to a database is ideal for repeating or structured data, such as
contact information. This page assumes…

developer.android.com

SOLID Design Principles Explained: Dependency Inversion Principle


with Code Examples
The SOLID design principles were promoted by Robert C. Martin and are some
of the best-known design principles in…
stackify.com

Unit tests with Mockito - Tutorial


Test doubles can be passed to other objects which are tested. Your tests can
validate that the class reacts correctly…
www.vogella.com

Save data in a local database using Room | Android Developers


Room provides an abstraction layer over SQLite to allow uent database
access while harnessing the full power of…
developer.android.com

Android App Development Architecture Components iOS Kotlin Mobile Development

About Help Legal

Anda mungkin juga menyukai