Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
### Добавить к запросу фактов запрос рандомных картинок с https://api.thecatapi.com/v1/images/search

1. На каждый рефреш экрана должен запрашиваться факт + картинка: добавляем сетевой запрос и реализуем логику аналогичную первой задаче. Для загрузки изображений уже подключена библиотека [Picasso](https://github.com/square/picasso)
2. В метод `view.populate` передаем 1 аргумент, поэтому необходимо реализовать модель презентейшен слоя в которой будут содержаться необходимые данные для рендеринга(текст и ссылка на картинку)
2. В метод `view.populate` передаем 1 аргумент, поэтому необходимо реализовать модель презентейшен слоя в которой будут содержаться необходимые данные для рендеринга (текст и ссылка на картинку)
3. Отменятся запросы должны одновременно

### Реализовать решение ViewModel

1. Реализовать наследника `ViewModel` и продублировать в нем логику из `CatsPresenter`, с необходимыми изменениями. Используйте `viewModelScope` в качестве скоупа.
2. Добавить логирование ошибок через CoroutineExceptionHanlder. Используйте класс CrashMonitor в качестве фейкового CrashMonitor инструмента
3. Создать sealed класс `Result`. Унаследовать от него классы `Success<T>`, `Error`. Использовать эти классы как стейт необходимый для рендеринга/отображени ошибки
3. Создать sealed класс `Result`. Унаследовать от него классы `Success<T>`, `Error`. Использовать эти классы как стейт необходимый для рендеринга/отображения ошибки
24 changes: 14 additions & 10 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {

android {
namespace = "otus.homework.coroutines"
compileSdkVersion 34
compileSdkVersion 36
defaultConfig {
applicationId "otus.homework.coroutines"
minSdkVersion 23
Expand All @@ -26,19 +26,23 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
kotlin {
jvmToolchain(21)
}
}

dependencies {
//noinspection GradleDependency
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.10'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.squareup.picasso:picasso:2.71828'

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4'
}
14 changes: 14 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CatImage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package otus.homework.coroutines

import com.google.gson.annotations.SerializedName

data class CatImage(
@SerializedName("url")
val url: String,
@SerializedName("width")
val width: Int,
@SerializedName("height")
val height: Int,
@SerializedName("id")
val id: String
)
6 changes: 6 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CatPresentation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

data class CatPresentation(
val fact: String,
val imageUrl: String
)
47 changes: 33 additions & 14 deletions app/src/main/java/otus/homework/coroutines/CatsPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.SocketTimeoutException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch

class CatsPresenter(
private val catsService: CatsService
private val catsService: CatsService,
private val catsImageService: CatsImageService
) {

private var _catsView: ICatsView? = null
private val presenterJob = Job()
private val presenterScope = CoroutineScope(
Dispatchers.Main + presenterJob + CoroutineName("CatsCoroutine")
)

fun onInitComplete() {
catsService.getCatFact().enqueue(object : Callback<Fact> {

override fun onResponse(call: Call<Fact>, response: Response<Fact>) {
if (response.isSuccessful && response.body() != null) {
_catsView?.populate(response.body()!!)
}
}

override fun onFailure(call: Call<Fact>, t: Throwable) {
presenterScope.launch {
try {
val factDeferred = async { catsService.getCatFact() }
val imageDeferred = async { catsImageService.getRandomCatImage() }

val fact = factDeferred.await()
val images = imageDeferred.await()

val imageUrl = images.firstOrNull()?.url
val catPresentation = CatPresentation(
fact = fact.fact,
imageUrl = imageUrl ?: ""
)
_catsView?.populate(catPresentation)
} catch (e: SocketTimeoutException) {
_catsView?.showError("Не удалось получить ответ от сервера")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

минорно, но лучше выносить в ресурсы

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вынес, и во ViewModel тоже.
Строковый ресурс, чтобы Context не тащить во ViewModel, через фабрику корректно передавать?
Или существуют способы изящнее?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

в целом я бы так не делал, но для домашки вполне рабочий вариант) можно обойтись без контекста, вынеся получение строк в какой то provider, в котором будет контекст уже

} catch (e: Exception) {

This comment was marked as resolved.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хоть CatsPresenter и не используется, поправил с учётом возможности CancellationException.
И для ViewModel тоже.

CrashMonitor.trackWarning()
_catsView?.showError(e.message ?: "Неизвестная ошибка")
}
})
}
}

fun attachView(catsView: ICatsView) {
Expand All @@ -31,5 +49,6 @@ class CatsPresenter(

fun detachView() {
_catsView = null
presenterJob.cancel()
}
}
9 changes: 7 additions & 2 deletions app/src/main/java/otus/homework/coroutines/CatsService.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.http.GET

interface CatsService {

@GET("fact")
fun getCatFact() : Call<Fact>
suspend fun getCatFact(): Fact
}

interface CatsImageService {

@GET("images/search")
suspend fun getRandomCatImage(): List<CatImage>
}
27 changes: 22 additions & 5 deletions app/src/main/java/otus/homework/coroutines/CatsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,47 @@ package otus.homework.coroutines
import android.content.Context
import android.util.AttributeSet
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import com.squareup.picasso.Picasso

class CatsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView {

var presenter :CatsPresenter? = null
var loadAction: (() -> Unit)? = null
// var presenter: CatsPresenter? = null

override fun onFinishInflate() {
super.onFinishInflate()
findViewById<Button>(R.id.button).setOnClickListener {
presenter?.onInitComplete()
loadAction?.invoke()
// presenter?.onInitComplete()
}
}

override fun populate(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.fact
override fun populate(catPresentation: CatPresentation) {
findViewById<TextView>(R.id.fact_textView).text = catPresentation.fact

if (catPresentation.imageUrl.isNotEmpty()) {
Picasso.get()
.load(catPresentation.imageUrl)
.into(findViewById<ImageView>(R.id.cat_imageView))
}
}

override fun showError(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

}

interface ICatsView {

fun populate(fact: Fact)
fun populate(catPresentation: CatPresentation)
fun showError(message: String)
}
51 changes: 51 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CatsViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package otus.homework.coroutines

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.net.SocketTimeoutException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.async
import kotlinx.coroutines.launch

class CatsViewModel(
private val catsService: CatsService,
private val catsImageService: CatsImageService
) : ViewModel() {

private val _catsLiveData = MutableLiveData<Result<CatPresentation>>()
val catsLiveData: LiveData<Result<CatPresentation>> = _catsLiveData

private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
CrashMonitor.trackWarning()
_catsLiveData.postValue(
Result.Error(exception as? Exception ?: Exception(exception.message))
)
}

fun loadCatData() {
viewModelScope.launch(exceptionHandler + CoroutineName("CatsCoroutine")) {
try {
val factDeferred = async { catsService.getCatFact() }
val imageDeferred = async { catsImageService.getRandomCatImage() }

val fact = factDeferred.await()
val images = imageDeferred.await()

val imageUrl = images.firstOrNull()?.url
val catPresentation = CatPresentation(
fact = fact.fact,
imageUrl = imageUrl ?: ""
)

_catsLiveData.value = Result.Success(catPresentation)
} catch (_: SocketTimeoutException) {
_catsLiveData.value = Result.Error(Exception("Не удалось получить ответ от сервера"))
} catch (e: Exception) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут дублирование с exceptionHandler, все будет перехвачено и в exceptionHandler уже ничего не попадет

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Всю обработку ошибок перенёс в CoroutineExceptionHandler

_catsLiveData.value = Result.Error(e)
}
}
}
}
12 changes: 10 additions & 2 deletions app/src/main/java/otus/homework/coroutines/DiContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@ import retrofit2.converter.gson.GsonConverterFactory

class DiContainer {

private val retrofit by lazy {
private val catsFactRetrofit by lazy {
Retrofit.Builder()
.baseUrl("https://catfact.ninja/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val service by lazy { retrofit.create(CatsService::class.java) }
private val catsImageRetrofit by lazy {
Retrofit.Builder()
.baseUrl("https://api.thecatapi.com/v1/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val catsService by lazy { catsFactRetrofit.create(CatsService::class.java) }
val catsImageService by lazy { catsImageRetrofit.create(CatsImageService::class.java) }
}
37 changes: 25 additions & 12 deletions app/src/main/java/otus/homework/coroutines/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package otus.homework.coroutines

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {

lateinit var catsPresenter: CatsPresenter
private lateinit var viewModel: CatsViewModel

private val diContainer = DiContainer()

Expand All @@ -15,16 +17,27 @@ class MainActivity : AppCompatActivity() {
val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView
setContentView(view)

catsPresenter = CatsPresenter(diContainer.service)
view.presenter = catsPresenter
catsPresenter.attachView(view)
catsPresenter.onInitComplete()
}

override fun onStop() {
if (isFinishing) {
catsPresenter.detachView()
viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CatsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return CatsViewModel(
diContainer.catsService,
diContainer.catsImageService
) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
})[CatsViewModel::class.java]

viewModel.catsLiveData.observe(this) { result ->
when (result) {
is Result.Success -> view.populate(result.data)
is Result.Error -> view.showError(result.exception.message ?: "Неизвестная ошибка")
}
}
super.onStop()

view.loadAction = { viewModel.loadCatData() }
viewModel.loadCatData()
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/otus/homework/coroutines/Result.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
15 changes: 12 additions & 3 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:padding="16dp"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">

<ImageView
android:id="@+id/cat_imageView"
android:layout_width="0dp"
android:layout_height="300dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/fact_textView"
android:textColor="@color/black"
android:textSize="24sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
Expand Down
Loading