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
18 changes: 10 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ plugins {
}

android {
compileSdkVersion 30
compileSdkVersion 33
buildToolsVersion "30.0.3"

defaultConfig {
applicationId "otus.homework.coroutines"
minSdkVersion 23
targetSdkVersion 30
targetSdkVersion 33
versionCode 1
versionName "1.0"

Expand All @@ -33,13 +33,15 @@ android {
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
}
8 changes: 8 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,8 @@
package otus.homework.coroutines

import com.google.gson.annotations.SerializedName

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

data class CatModel(
val fact: String,
val url: String
)
35 changes: 20 additions & 15 deletions app/src/main/java/otus/homework/coroutines/CatsPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlinx.coroutines.*
import java.net.SocketTimeoutException

class CatsPresenter(
private val catsService: CatsService
private val serviceCatFact: CatsService,
private val serviceCatImage: CatsService
) {

private var _catsView: ICatsView? = null
private val outerScope = PresenterScope()
private lateinit var innerScope: CoroutineScope

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) {
CrashMonitor.trackWarning()
outerScope.launch {
innerScope = CoroutineScope(Dispatchers.IO)
Copy link
Contributor

Choose a reason for hiding this comment

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

А иннер скоуп ты зачем делаешь?

try {
val catFact = innerScope.async { serviceCatFact.getCatFact() }
val catImage = innerScope.async { serviceCatImage.getCatImage() }
_catsView?.populate(CatModel(catFact.await().fact, catImage.await().url))
} catch (exc: SocketTimeoutException) {
_catsView?.makeToast("Не удалось получить ответ от сервером")
} catch (exc: Exception) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Здесь будет также пойман CancellationException, через который работает механизм отмены. Нужно его пробросить дальше, либо не ловить такой общий тип исключений

CrashMonitor.trackWarning(exc)
_catsView?.makeToast(exc.message.toString())
}
})
}
}

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

fun detachView() {
_catsView = null
outerScope.cancel()
innerScope.cancel()
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CatsResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

sealed interface CatsResult {
class Success<T> (val result: T): CatsResult
class Error(val exc: String): CatsResult
}
6 changes: 4 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,12 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.http.GET

interface CatsService {

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

@GET("meow")
suspend fun getCatImage(): CatImage
}
22 changes: 17 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,42 @@ 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 viewModel :CatsViewModel? = null

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

override fun populate(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.text
override fun populate(model: CatModel) {
findViewById<TextView>(R.id.fact_textView).text = model.fact
Picasso.get()
.load(model.url)
.into(findViewById<ImageView>(R.id.cat_image))
}

override fun makeToast(text: String) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
}

interface ICatsView {

fun populate(fact: Fact)
fun populate(model: CatModel)

fun makeToast(text: String)
}
29 changes: 29 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,29 @@
package otus.homework.coroutines

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.async
import kotlinx.coroutines.launch

class CatsViewModel(
private val serviceCatFact: CatsService,
private val serviceCatImage: CatsService
): ViewModel() {

var result: MutableLiveData<CatsResult> = MutableLiveData()
Copy link
Contributor

Choose a reason for hiding this comment

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

Наружу нужно показывать иммутабельную версию лайвдаты/стейтфлоу


private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
CrashMonitor.trackWarning(throwable)
result.postValue(CatsResult.Error(throwable.message.toString()))
}

fun onInitComplete() {
viewModelScope.launch(exceptionHandler) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Добавь обработку исключений через try/catch

val catFact = async { serviceCatFact.getCatFact() }
val catImage = async { serviceCatImage.getCatImage() }
result.postValue(CatsResult.Success(CatModel(catFact.await().fact, catImage.await().url)))
Copy link
Contributor

Choose a reason for hiding this comment

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

А зачем тут postValue, ты же уже на главный поток вернулась

}
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/otus/homework/coroutines/CrashMonitor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ object CrashMonitor {
/**
* Pretend this is Crashlytics/AppCenter
*/
fun trackWarning() {
fun trackWarning(e: Throwable) {
}
}
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 retrofitCatFact by lazy {
Retrofit.Builder()
.baseUrl("https://catfact.ninja/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val service by lazy { retrofit.create(CatsService::class.java) }
private val retrofitCatImage by lazy {
Retrofit.Builder()
.baseUrl("https://aws.random.cat/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val serviceCatFact: CatsService by lazy { retrofitCatFact.create(CatsService::class.java) }
val serviceCatImage: CatsService by lazy { retrofitCatImage.create(CatsService::class.java) }
}
22 changes: 4 additions & 18 deletions app/src/main/java/otus/homework/coroutines/Fact.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,8 @@ package otus.homework.coroutines
import com.google.gson.annotations.SerializedName

data class Fact(
@field:SerializedName("createdAt")
val createdAt: String,
@field:SerializedName("deleted")
val deleted: Boolean,
@field:SerializedName("_id")
val id: String,
@field:SerializedName("text")
val text: String,
@field:SerializedName("source")
val source: String,
@field:SerializedName("used")
val used: Boolean,
@field:SerializedName("type")
val type: String,
@field:SerializedName("user")
val user: String,
@field:SerializedName("updatedAt")
val updatedAt: String
@field:SerializedName("fact")
val fact: String,
@field:SerializedName("length")
val length: Int
)
22 changes: 9 additions & 13 deletions app/src/main/java/otus/homework/coroutines/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
package otus.homework.coroutines

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

lateinit var catsPresenter: CatsPresenter

private val diContainer = DiContainer()
private var catsViewModel = CatsViewModel(diContainer.serviceCatFact, diContainer.serviceCatImage)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

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()
view.viewModel = catsViewModel
catsViewModel.result.observe(this) {
when(it) {
is CatsResult.Success<*> -> view.populate(it.result as CatModel)
is CatsResult.Error -> view.makeToast(it.exc)
}
}
super.onStop()
catsViewModel.onInitComplete()
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/otus/homework/coroutines/PresenterScope.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package otus.homework.coroutines

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext

class PresenterScope: CoroutineScope {

override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + CoroutineName("CatsCoroutine")
}
9 changes: 9 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
android:padding="16dp"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageView
android:id="@+id/cat_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/fact_textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/fact_textView"
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.6.20"
ext.kotlin_version = "1.8.10"
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

// NOTE: Do not place your application dependencies here; they belong
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip