Skip to content
Open
Show file tree
Hide file tree
Changes from all 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`. Использовать эти классы как стейт необходимый для рендеринга/отображения ошибки
28 changes: 16 additions & 12 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 @@ -23,22 +23,26 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
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
)
50 changes: 36 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,49 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.net.SocketTimeoutException
import kotlin.coroutines.cancellation.CancellationException
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: CancellationException) {
throw e
} catch (e: SocketTimeoutException) {
_catsView?.showError(e)
} 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)
}
})
}
}

fun attachView(catsView: ICatsView) {
Expand All @@ -31,5 +52,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>
}
30 changes: 25 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,50 @@ 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
import java.net.SocketTimeoutException

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

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

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: Exception) {
val text = when (message) {
is SocketTimeoutException -> context.getString(R.string.error_timeout)
else -> message.message ?: context.getString(R.string.error_unknown)
}
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}

}

interface ICatsView {

fun populate(fact: Fact)
fun populate(catPresentation: CatPresentation)
fun showError(message: Exception)
}
50 changes: 50 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,50 @@
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 kotlin.coroutines.cancellation.CancellationException
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,
private val timeoutErrorMessage: String
) : ViewModel() {

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

private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
CrashMonitor.trackWarning()
val error = when (exception) {
is SocketTimeoutException -> Exception(timeoutErrorMessage)
is CancellationException -> exception
else -> exception as? Exception ?: Exception(exception.message)
}
_catsLiveData.postValue(Result.Error(error))
}

fun loadCatData() {
viewModelScope.launch(exceptionHandler + CoroutineName("CatsCoroutine")) {
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)
}
}
}
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) }
}
38 changes: 26 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,28 @@ 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,
getString(R.string.error_timeout)
) 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)
}
}
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>()
}
Loading