Skip to content

Commit 24126f5

Browse files
authored
Updating Retrofit interfaces and ViewModel (#43)
* Converting Retrofit interface functions to suspend functions. Converting AndroidViewModel to ViewModel. * Refactoring DI (Retrofit), removing StringResProvider, refactoring message LiveData.
1 parent c4be99b commit 24126f5

File tree

13 files changed

+192
-116
lines changed

13 files changed

+192
-116
lines changed

app/build.gradle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ ext {
6969
timber_version = "4.7.1"
7070
moshi_version = "1.11.0"
7171
retrofit_version = "2.9.0"
72-
coroutine_adapter_version = "0.9.2"
7372
leak_canary_version = "1.6.3"
7473
lifecycle_extensions = "2.2.0"
7574
lifecycle_version = "2.3.0"
@@ -106,7 +105,6 @@ dependencies {
106105
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
107106
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
108107
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
109-
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$coroutine_adapter_version"
110108

111109
// Kotlin Coroutines
112110
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.rocketinsights.android.coroutines
2+
3+
import kotlin.coroutines.CoroutineContext
4+
5+
interface DispatcherProvider {
6+
7+
fun main(): CoroutineContext
8+
9+
fun io(): CoroutineContext
10+
11+
fun default(): CoroutineContext
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.rocketinsights.android.coroutines
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlin.coroutines.CoroutineContext
5+
6+
class DispatcherProviderImpl : DispatcherProvider {
7+
8+
override fun main(): CoroutineContext = Dispatchers.Main
9+
10+
override fun io(): CoroutineContext = Dispatchers.IO
11+
12+
override fun default(): CoroutineContext = Dispatchers.Default
13+
}
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package com.rocketinsights.android.di
22

33
import android.app.Application
4+
import com.rocketinsights.android.coroutines.DispatcherProvider
5+
import com.rocketinsights.android.coroutines.DispatcherProviderImpl
46
import com.rocketinsights.android.network.ApiService
57
import com.rocketinsights.android.repos.MessageRepository
68
import com.rocketinsights.android.viewmodels.MainViewModel
79
import org.koin.android.ext.koin.androidContext
810
import org.koin.androidx.viewmodel.dsl.viewModel
911
import org.koin.core.context.startKoin
1012
import org.koin.dsl.module
13+
import retrofit2.Retrofit
14+
import retrofit2.converter.moshi.MoshiConverterFactory
15+
16+
private const val API_BASE_URL = "http://www.mocky.io/v2/"
1117

1218
fun Application.initKoin() {
1319
startKoin {
@@ -17,13 +23,23 @@ fun Application.initKoin() {
1723
}
1824

1925
private fun networkModule() = module {
20-
single { ApiService.getApiService() }
26+
single<Retrofit> {
27+
Retrofit.Builder()
28+
.baseUrl(API_BASE_URL)
29+
.addConverterFactory(MoshiConverterFactory.create())
30+
.build()
31+
}
32+
33+
single<ApiService> {
34+
get<Retrofit>().create(ApiService::class.java)
35+
}
2136
}
2237

2338
private fun repositoryModule() = module {
24-
single { MessageRepository(get()) }
39+
single<DispatcherProvider> { DispatcherProviderImpl() }
40+
single { MessageRepository(get(), get()) }
2541
}
2642

2743
private fun scopeModules() = module {
28-
viewModel { MainViewModel(get(), get()) }
44+
viewModel { MainViewModel(get()) }
2945
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.rocketinsights.android.extensions
2+
3+
import android.view.View
4+
import android.view.View.VISIBLE
5+
import android.view.View.INVISIBLE
6+
import android.view.View.GONE
7+
8+
fun View.show() {
9+
if (visibility == VISIBLE) return
10+
visibility = VISIBLE
11+
}
12+
13+
fun View.hide() {
14+
if (visibility == INVISIBLE) return
15+
visibility = INVISIBLE
16+
}
17+
18+
fun View.remove() {
19+
if (visibility == GONE) return
20+
visibility = GONE
21+
}
22+
23+
fun View.enable() {
24+
if (isEnabled) return
25+
isEnabled = true
26+
}
27+
28+
fun View.disable() {
29+
if (!isEnabled) return
30+
isEnabled = false
31+
}
Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
package com.rocketinsights.android.network
22

3-
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
43
import com.rocketinsights.android.models.Message
5-
import kotlinx.coroutines.Deferred
6-
import retrofit2.Retrofit
7-
import retrofit2.converter.moshi.MoshiConverterFactory
84
import retrofit2.http.GET
95

106
interface ApiService {
117
@GET("5cd9d1383000004a20c01850")
12-
fun getMessageAsync(): Deferred<Message>
13-
14-
companion object {
15-
fun getApiService(): ApiService = Retrofit.Builder()
16-
.baseUrl("http://www.mocky.io/v2/")
17-
.addConverterFactory(MoshiConverterFactory.create())
18-
.addCallAdapterFactory(CoroutineCallAdapterFactory())
19-
.build().create(ApiService::class.java)
20-
}
8+
suspend fun getMessage(): Message
219
}
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.rocketinsights.android.repos
22

3+
import com.rocketinsights.android.coroutines.DispatcherProvider
4+
import com.rocketinsights.android.models.Message
35
import com.rocketinsights.android.network.ApiService
6+
import kotlinx.coroutines.withContext
47

5-
class MessageRepository(private val api: ApiService) {
6-
fun getMessageAsync() = api.getMessageAsync()
8+
class MessageRepository(private val api: ApiService, private val dispatcher: DispatcherProvider) {
9+
suspend fun getMessage(): Message = withContext(dispatcher.io()) { api.getMessage() }
710
}

app/src/main/java/com/rocketinsights/android/ui/MainFragment.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import androidx.navigation.fragment.findNavController
88
import com.bumptech.glide.Glide
99
import com.rocketinsights.android.R
1010
import com.rocketinsights.android.databinding.FragmentMainBinding
11+
import com.rocketinsights.android.extensions.show
1112
import com.rocketinsights.android.extensions.viewBinding
13+
import com.rocketinsights.android.viewmodels.MainFragmentMessage
1214
import com.rocketinsights.android.viewmodels.MainViewModel
1315
import org.koin.androidx.viewmodel.ext.android.viewModel
16+
import retrofit2.HttpException
1417

1518
class MainFragment : Fragment(R.layout.fragment_main) {
1619
private val mainViewModel: MainViewModel by viewModel()
@@ -34,8 +37,15 @@ class MainFragment : Fragment(R.layout.fragment_main) {
3437

3538
private fun setupObservers() {
3639
mainViewModel.message.observe(viewLifecycleOwner, { data ->
37-
binding.message.text = data?.text
38-
binding.stockImage.visibility = View.VISIBLE
40+
when (data) {
41+
is MainFragmentMessage.Loading -> binding.message.text = getString(R.string.loading)
42+
is MainFragmentMessage.Success -> binding.message.text = data.message.text
43+
is MainFragmentMessage.Error -> binding.message.text =
44+
if (data.exception is HttpException) getString(R.string.http_error)
45+
else getString(R.string.unknown_error)
46+
}
47+
48+
binding.stockImage.show()
3949

4050
binding.message.setOnClickListener {
4151
findNavController().navigate(MainFragmentDirections.actionSlideTransition())
Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
11
package com.rocketinsights.android.viewmodels
22

3-
import android.app.Application
4-
import androidx.lifecycle.AndroidViewModel
3+
import androidx.lifecycle.LiveData
54
import androidx.lifecycle.MutableLiveData
5+
import androidx.lifecycle.ViewModel
66
import androidx.lifecycle.viewModelScope
7-
import com.rocketinsights.android.R
87
import com.rocketinsights.android.models.Message
98
import com.rocketinsights.android.repos.MessageRepository
109
import kotlinx.coroutines.delay
1110
import kotlinx.coroutines.launch
12-
import retrofit2.HttpException
11+
import timber.log.Timber
12+
13+
private const val ERROR_GET_MESSAGE = "Error getting messages from the remote API."
1314

1415
class MainViewModel(
15-
application: Application,
1616
private val repo: MessageRepository
17-
) : AndroidViewModel(application) {
18-
val message = MutableLiveData<Message>()
17+
) : ViewModel() {
18+
19+
private val _message = MutableLiveData<MainFragmentMessage>(MainFragmentMessage.Loading)
20+
val message: LiveData<MainFragmentMessage> = _message
1921

2022
init {
21-
message.postValue(Message(application.getString(R.string.loading)))
2223
viewModelScope.launch {
23-
delay(2000)
24+
delay(2000) // just to simulate 2 sec delay - remove from production code
2425
try {
25-
val m = repo.getMessageAsync().await()
26-
message.postValue(m)
27-
} catch (e: HttpException) {
28-
message.postValue(Message(application.getString(R.string.http_error)))
26+
val m = repo.getMessage()
27+
_message.value = MainFragmentMessage.Success(m)
2928
} catch (e: Throwable) {
30-
message.postValue(Message(application.getString(R.string.unknown_error)))
29+
_message.value = MainFragmentMessage.Error(e)
30+
Timber.e(e, ERROR_GET_MESSAGE)
3131
}
3232
}
3333
}
34+
}
35+
36+
sealed class MainFragmentMessage {
37+
object Loading : MainFragmentMessage()
38+
data class Success(val message: Message) : MainFragmentMessage()
39+
data class Error(val exception: Throwable) : MainFragmentMessage()
3440
}

app/src/test/java/com/rocketinsights/android/ExampleUnitTest.kt

Lines changed: 0 additions & 17 deletions
This file was deleted.

app/src/test/java/com/rocketinsights/android/ext/TestExtensions.kt

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,46 @@
11
package com.rocketinsights.android.repos
22

3-
import com.nhaarman.mockitokotlin2.doReturn
43
import com.nhaarman.mockitokotlin2.mock
5-
import com.rocketinsights.android.ext.toDeferred
4+
import com.nhaarman.mockitokotlin2.verify
5+
import com.nhaarman.mockitokotlin2.whenever
6+
import com.rocketinsights.android.coroutines.DispatcherProvider
67
import com.rocketinsights.android.models.Message
78
import com.rocketinsights.android.network.ApiService
89
import kotlinx.coroutines.Dispatchers
910
import kotlinx.coroutines.ExperimentalCoroutinesApi
10-
import kotlinx.coroutines.asCoroutineDispatcher
11+
import kotlinx.coroutines.test.TestCoroutineDispatcher
12+
import kotlinx.coroutines.test.TestCoroutineScope
13+
import kotlinx.coroutines.test.runBlockingTest
1114
import kotlinx.coroutines.test.setMain
12-
import org.junit.Assert
13-
import org.junit.Test
14-
15+
import org.junit.Assert.assertEquals
1516
import org.junit.Before
16-
import java.util.concurrent.Executors
17+
import org.junit.Test
1718

1819
@ExperimentalCoroutinesApi
1920
class MessageRepositoryTest {
20-
private val api = mock<ApiService> {
21-
on { getMessageAsync() } doReturn Message("Done!").toDeferred()
22-
}
21+
22+
private val testDispatcher = TestCoroutineDispatcher()
23+
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
24+
private val api = mock<ApiService>()
25+
private val dispatcher = mock<DispatcherProvider>()
26+
private val repo by lazy { MessageRepository(api, dispatcher) }
2327

2428
@Before
2529
fun setUp() {
26-
Dispatchers.setMain(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
30+
Dispatchers.setMain(testDispatcher)
2731
}
2832

2933
@Test
30-
fun getMessageAsync() {
31-
val repo = MessageRepository(api)
32-
Assert.assertEquals("Done!", repo.getMessageAsync().getCompleted().text)
34+
fun getMessage() = testCoroutineScope.runBlockingTest {
35+
// arrange
36+
whenever(dispatcher.io()).thenReturn(testDispatcher)
37+
whenever(api.getMessage()).thenReturn(Message("Done!"))
38+
39+
// act
40+
val message = repo.getMessage()
41+
42+
// assert
43+
verify(api).getMessage()
44+
assertEquals("Done!", message.text)
3345
}
3446
}

0 commit comments

Comments
 (0)