Skip to content

Commit df9049e

Browse files
author
Artem Nagorny
committed
homework solution Otus-Android#1
1 parent bc8b28c commit df9049e

File tree

14 files changed

+276
-25
lines changed

14 files changed

+276
-25
lines changed

app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ dependencies {
4141
implementation 'com.google.android.material:material:1.11.0'
4242
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
4343
implementation 'com.squareup.picasso:picasso:2.71828'
44+
implementation 'androidx.activity:activity-ktx:1.3.1'
45+
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
4446
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package otus.homework.coroutines
2+
3+
data class Cat(
4+
val fact: Fact,
5+
val presentation: Presentation,
6+
)
Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
11
package otus.homework.coroutines
22

3-
import retrofit2.Call
4-
import retrofit2.Callback
5-
import retrofit2.Response
3+
import android.content.Context
4+
import android.widget.Toast
5+
import kotlinx.coroutines.CoroutineScope
6+
import kotlinx.coroutines.Job
7+
import kotlinx.coroutines.launch
8+
import java.net.SocketTimeoutException
69

710
class CatsPresenter(
8-
private val catsService: CatsService
11+
private val context: Context,
12+
private val catsRepository: ICatsRepository,
913
) {
10-
14+
private var _catsJob: Job? = null
1115
private var _catsView: ICatsView? = null
1216

13-
fun onInitComplete() {
14-
catsService.getCatFact().enqueue(object : Callback<Fact> {
15-
16-
override fun onResponse(call: Call<Fact>, response: Response<Fact>) {
17-
if (response.isSuccessful && response.body() != null) {
18-
_catsView?.populate(response.body()!!)
19-
}
20-
}
17+
private val scope: CoroutineScope by lazy { PresenterScope() }
2118

22-
override fun onFailure(call: Call<Fact>, t: Throwable) {
19+
fun onInitComplete() {
20+
cancelCatJob()
21+
_catsJob = scope.launch {
22+
try {
23+
val cat: Cat = catsRepository.getCat()
24+
25+
_catsView?.populate(cat)
26+
} catch (e: SocketTimeoutException) {
27+
Toast.makeText(
28+
context,
29+
"Не удалось получить ответ от сервера",
30+
Toast.LENGTH_SHORT,
31+
).show()
32+
} catch (e: Exception) {
33+
e.printStackTrace()
2334
CrashMonitor.trackWarning()
35+
e.message?.let { eMessage ->
36+
Toast.makeText(
37+
context,
38+
eMessage,
39+
Toast.LENGTH_SHORT,
40+
).show()
41+
}
2442
}
25-
})
43+
}
2644
}
2745

2846
fun attachView(catsView: ICatsView) {
@@ -32,4 +50,13 @@ class CatsPresenter(
3250
fun detachView() {
3351
_catsView = null
3452
}
53+
54+
fun dispose() {
55+
cancelCatJob()
56+
}
57+
58+
private fun cancelCatJob() {
59+
_catsJob?.cancel()
60+
_catsJob = null
61+
}
3562
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package otus.homework.coroutines
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.async
5+
import kotlinx.coroutines.withContext
6+
7+
interface ICatsRepository {
8+
suspend fun getCat(): Cat
9+
}
10+
11+
class CatsRepositoryImpl(
12+
private val service: CatsService,
13+
) : ICatsRepository {
14+
override suspend fun getCat(): Cat = withContext(Dispatchers.IO) {
15+
val defFact = async {
16+
getCatFact()
17+
}
18+
val defPresentation = async {
19+
getCatPresentation()
20+
}
21+
val (fact, presentation) = Pair(defFact.await(), defPresentation.await())
22+
Cat(
23+
fact = fact,
24+
presentation = presentation,
25+
)
26+
}
27+
28+
private suspend fun getCatFact(): Fact {
29+
return service.getCatFact(Endpoints.FACTS_BASE_URL + "fact").toDomain()
30+
}
31+
32+
private suspend fun getCatPresentation(): Presentation {
33+
return service.getCatPresentation(Endpoints.PRESENTATION_BASE_URL)
34+
.firstOrNull()?.toDomain()
35+
?: throw Exception("first presentation not found")
36+
}
37+
}
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package otus.homework.coroutines
22

3-
import retrofit2.Call
43
import retrofit2.http.GET
4+
import retrofit2.http.Url
55

66
interface CatsService {
7+
// @Url так как разные домены у поставщиков для фактов и изображений
8+
// как вариант, ещё можно было сделать два отдельных сервиса
9+
// Например: CatFactsService, CatPresentationService (для каждого свой экземпляр retrofit)
10+
@GET
11+
suspend fun getCatFact(@Url url: String) : FactDTO
712

8-
@GET("fact")
9-
fun getCatFact() : Call<Fact>
13+
@GET
14+
suspend fun getCatPresentation(@Url url: String) : List<PresentationDTO>
1015
}

app/src/main/java/otus/homework/coroutines/CatsView.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package otus.homework.coroutines
33
import android.content.Context
44
import android.util.AttributeSet
55
import android.widget.Button
6+
import android.widget.ImageView
67
import android.widget.TextView
78
import androidx.constraintlayout.widget.ConstraintLayout
9+
import com.squareup.picasso.Picasso
810

911
class CatsView @JvmOverloads constructor(
1012
context: Context,
@@ -21,12 +23,23 @@ class CatsView @JvmOverloads constructor(
2123
}
2224
}
2325

24-
override fun populate(fact: Fact) {
26+
override fun populate(cat: Cat) {
27+
setFact(cat.fact)
28+
setPicture(cat.presentation)
29+
}
30+
31+
private fun setFact(fact: Fact) {
2532
findViewById<TextView>(R.id.fact_textView).text = fact.fact
2633
}
34+
35+
private fun setPicture(presentation: Presentation) {
36+
Picasso.get()
37+
.load(presentation.url)
38+
.into(findViewById<ImageView>(R.id.picture_imageView))
39+
}
2740
}
2841

2942
interface ICatsView {
3043

31-
fun populate(fact: Fact)
44+
fun populate(cat: Cat)
3245
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package otus.homework.coroutines
2+
3+
import androidx.lifecycle.MutableLiveData
4+
import androidx.lifecycle.ViewModel
5+
import androidx.lifecycle.ViewModelProvider
6+
import androidx.lifecycle.viewModelScope
7+
import kotlinx.coroutines.CancellationException
8+
import kotlinx.coroutines.CoroutineExceptionHandler
9+
import kotlinx.coroutines.Job
10+
import kotlinx.coroutines.launch
11+
import java.net.SocketTimeoutException
12+
13+
class CatsViewModel(
14+
private val catsRepository: ICatsRepository,
15+
): ViewModel() {
16+
private var _catsJob: Job? = null
17+
18+
val uiState = MutableLiveData<Result<Cat>>()
19+
20+
private val exceptionHandle = CoroutineExceptionHandler { _, throwable ->
21+
when (throwable) {
22+
is CancellationException -> {
23+
// ...
24+
}
25+
is SocketTimeoutException -> {
26+
uiState.value = Error("Не удалось получить ответ от сервера")
27+
}
28+
else -> {
29+
throwable.printStackTrace()
30+
CrashMonitor.trackWarning()
31+
throwable.message?.let { eMessage ->
32+
uiState.value = Error(eMessage)
33+
}
34+
}
35+
}
36+
}
37+
38+
override fun onCleared() {
39+
super.onCleared()
40+
cancelCatJob()
41+
}
42+
43+
fun onInitComplete() {
44+
cancelCatJob()
45+
_catsJob = viewModelScope.launch(exceptionHandle) {
46+
val cat: Cat = catsRepository.getCat()
47+
uiState.value = Success(cat)
48+
}
49+
}
50+
51+
private fun cancelCatJob() {
52+
_catsJob?.cancel()
53+
_catsJob = null
54+
}
55+
56+
class Factory(
57+
private val catsRepository: ICatsRepository,
58+
): ViewModelProvider.Factory {
59+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
60+
return CatsViewModel(
61+
catsRepository = catsRepository,
62+
) as T
63+
}
64+
}
65+
}

app/src/main/java/otus/homework/coroutines/DiContainer.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,23 @@ class DiContainer {
77

88
private val retrofit by lazy {
99
Retrofit.Builder()
10-
.baseUrl("https://catfact.ninja/")
10+
// Описал причину такого безобразия в CatsService)
11+
.baseUrl(Endpoints.STUB)
1112
.addConverterFactory(GsonConverterFactory.create())
1213
.build()
1314
}
1415

1516
val service by lazy { retrofit.create(CatsService::class.java) }
17+
18+
val repository: ICatsRepository by lazy {
19+
CatsRepositoryImpl(
20+
service = service,
21+
)
22+
}
23+
}
24+
25+
object Endpoints {
26+
const val STUB = "https://stub.com"
27+
const val FACTS_BASE_URL: String = "https://catfact.ninja/"
28+
const val PRESENTATION_BASE_URL: String = "https://api.thecatapi.com/v1/images/search"
1629
}

app/src/main/java/otus/homework/coroutines/Fact.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ package otus.homework.coroutines
33
import com.google.gson.annotations.SerializedName
44

55
data class Fact(
6+
val fact: String,
7+
)
8+
9+
data class FactDTO(
610
@field:SerializedName("fact")
711
val fact: String,
812
@field:SerializedName("length")
9-
val length: Int
13+
val length: Int,
14+
)
15+
fun FactDTO.toDomain() = Fact(
16+
fact = fact,
1017
)

app/src/main/java/otus/homework/coroutines/MainActivity.kt

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,55 @@ package otus.homework.coroutines
22

33
import androidx.appcompat.app.AppCompatActivity
44
import android.os.Bundle
5+
import android.widget.Toast
6+
import androidx.activity.viewModels
57

68
class MainActivity : AppCompatActivity() {
79

810
lateinit var catsPresenter: CatsPresenter
911

1012
private val diContainer = DiContainer()
1113

14+
private val catsViewModel: CatsViewModel by viewModels {
15+
CatsViewModel.Factory(
16+
catsRepository = diContainer.repository,
17+
)
18+
}
19+
1220
override fun onCreate(savedInstanceState: Bundle?) {
1321
super.onCreate(savedInstanceState)
1422

1523
val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView
1624
setContentView(view)
1725

18-
catsPresenter = CatsPresenter(diContainer.service)
26+
catsPresenter = CatsPresenter(
27+
context = applicationContext,
28+
catsRepository = diContainer.repository,
29+
)
1930
view.presenter = catsPresenter
20-
catsPresenter.attachView(view)
21-
catsPresenter.onInitComplete()
31+
// catsPresenter.attachView(view)
32+
// catsPresenter.onInitComplete()
33+
34+
catsViewModel.uiState.observe(this) { result: Result<Cat> ->
35+
when (result) {
36+
is Success -> view.populate(result.data)
37+
is Error -> {
38+
Toast.makeText(
39+
this,
40+
result.message,
41+
Toast.LENGTH_SHORT,
42+
).show()
43+
}
44+
}
45+
}
46+
catsViewModel.onInitComplete()
2247
}
2348

2449
override fun onStop() {
2550
if (isFinishing) {
2651
catsPresenter.detachView()
2752
}
53+
catsPresenter.dispose()
2854
super.onStop()
2955
}
3056
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package otus.homework.coroutines
2+
3+
import com.google.gson.annotations.SerializedName
4+
5+
data class Presentation(
6+
val url: String,
7+
)
8+
9+
data class PresentationDTO(
10+
@field:SerializedName("id")
11+
val id: String,
12+
@field:SerializedName("url")
13+
val url: String,
14+
)
15+
16+
fun PresentationDTO.toDomain() = Presentation(
17+
url = url,
18+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package otus.homework.coroutines
2+
3+
import kotlinx.coroutines.CoroutineName
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlin.coroutines.CoroutineContext
7+
8+
class PresenterScope : CoroutineScope {
9+
override val coroutineContext: CoroutineContext
10+
get() = Dispatchers.Main + CoroutineName(NAME)
11+
12+
companion object {
13+
const val NAME = "CatsCoroutine"
14+
}
15+
}

0 commit comments

Comments
 (0)