From 10c6ac2741aebb33ec64a1cbb8acfd6f6d745fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=90=D0=B3=D0=B0=D1=84=D0=BE?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Mon, 20 Jan 2025 00:21:09 +0300 Subject: [PATCH 1/3] =?UTF-8?q?1.=20=D0=9F=D0=B5=D1=80=D0=B5=D0=B9=D1=82?= =?UTF-8?q?=D0=B8=20=D1=81=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B1=D0=B5=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D1=81=D0=B0=D1=81=D0=BF=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D1=80=D1=83=D1=82=D0=B8=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 1 + .../otus/homework/coroutines/CatsPresenter.kt | 50 +++++++++++++------ .../otus/homework/coroutines/CatsService.kt | 4 +- .../java/otus/homework/coroutines/CatsView.kt | 8 ++- .../otus/homework/coroutines/CrashMonitor.kt | 5 +- .../otus/homework/coroutines/DiContainer.kt | 19 ++++++- .../java/otus/homework/coroutines/Fact.kt | 10 ++-- .../otus/homework/coroutines/MainActivity.kt | 19 +++++-- 8 files changed, 85 insertions(+), 31 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a414e0e8..1acdb432 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,4 +41,5 @@ dependencies { implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.squareup.picasso:picasso:2.71828' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt index e4b05120..8d555188 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt @@ -1,28 +1,42 @@ package otus.homework.coroutines -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.SocketTimeoutException + class CatsPresenter( - private val catsService: CatsService + private val catsService: CatsService, + private val presenterScope: CoroutineScope ) { - private var _catsView: ICatsView? = null + private var job: Job? = null fun onInitComplete() { - catsService.getCatFact().enqueue(object : Callback { - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - _catsView?.populate(response.body()!!) + job?.cancel() + job = presenterScope.launch { + runCatching { catsService.getCatFact() } + .onSuccess { response -> + if (response.isSuccessful && response.body() != null) { + withContext(Dispatchers.Main) { _catsView?.populate(response.body()!!) } + } } - } - - override fun onFailure(call: Call, t: Throwable) { - CrashMonitor.trackWarning() - } - }) + .onFailure { t -> + if (t is SocketTimeoutException) { + withContext(Dispatchers.Main) { _catsView?.showToast("Не удалось получить ответ от сервером") } + } else { + withContext(Dispatchers.Main) { + _catsView?.showToast( + t.message ?: "No message" + ) + } + CrashMonitor.trackWarning(t) + } + } + } } fun attachView(catsView: ICatsView) { @@ -32,4 +46,8 @@ class CatsPresenter( fun detachView() { _catsView = null } + + fun cancelJob() { + job?.cancel() + } } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsService.kt b/app/src/main/java/otus/homework/coroutines/CatsService.kt index 479b2cfb..89a65c3b 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsService.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsService.kt @@ -1,10 +1,10 @@ package otus.homework.coroutines -import retrofit2.Call +import retrofit2.Response import retrofit2.http.GET interface CatsService { @GET("fact") - fun getCatFact() : Call + suspend fun getCatFact(): Response } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsView.kt b/app/src/main/java/otus/homework/coroutines/CatsView.kt index be04b2a8..0d404f61 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsView.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsView.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.AttributeSet import android.widget.Button import android.widget.TextView +import android.widget.Toast import androidx.constraintlayout.widget.ConstraintLayout class CatsView @JvmOverloads constructor( @@ -12,7 +13,7 @@ class CatsView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView { - var presenter :CatsPresenter? = null + var presenter: CatsPresenter? = null override fun onFinishInflate() { super.onFinishInflate() @@ -24,9 +25,14 @@ class CatsView @JvmOverloads constructor( override fun populate(fact: Fact) { findViewById(R.id.fact_textView).text = fact.fact } + + override fun showToast(text: String) { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } } interface ICatsView { fun populate(fact: Fact) + fun showToast(text: String) } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt b/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt index 32e6b018..2d4c7fd8 100644 --- a/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt +++ b/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt @@ -1,10 +1,13 @@ package otus.homework.coroutines +import android.util.Log + object CrashMonitor { /** * Pretend this is Crashlytics/AppCenter */ - fun trackWarning() { + fun trackWarning(t: Throwable) { + Log.d("CatsApp", t.toString()) } } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/DiContainer.kt b/app/src/main/java/otus/homework/coroutines/DiContainer.kt index 23ddc3b2..6bf94ea8 100644 --- a/app/src/main/java/otus/homework/coroutines/DiContainer.kt +++ b/app/src/main/java/otus/homework/coroutines/DiContainer.kt @@ -1,9 +1,16 @@ package otus.homework.coroutines +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.job import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.Executors -class DiContainer { +class DiContainer(private val lifecycle: Lifecycle) { private val retrofit by lazy { Retrofit.Builder() @@ -12,5 +19,13 @@ class DiContainer { .build() } - val service by lazy { retrofit.create(CatsService::class.java) } + val service: CatsService by lazy { retrofit.create(CatsService::class.java) } + + val presenterScope by lazy { + val mainDispatcher = Executors + .newSingleThreadExecutor() + .asCoroutineDispatcher() + + CoroutineScope(CoroutineName("CatsCoroutine") + mainDispatcher + lifecycle.coroutineScope.coroutineContext.job) + } } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/Fact.kt b/app/src/main/java/otus/homework/coroutines/Fact.kt index 643a5a33..e30e2fc1 100644 --- a/app/src/main/java/otus/homework/coroutines/Fact.kt +++ b/app/src/main/java/otus/homework/coroutines/Fact.kt @@ -3,8 +3,8 @@ package otus.homework.coroutines import com.google.gson.annotations.SerializedName data class Fact( - @field:SerializedName("fact") - val fact: String, - @field:SerializedName("length") - val length: Int -) \ No newline at end of file + @field:SerializedName("fact") + val fact: String, + @field:SerializedName("length") + val length: Int +) diff --git a/app/src/main/java/otus/homework/coroutines/MainActivity.kt b/app/src/main/java/otus/homework/coroutines/MainActivity.kt index a9dafb3b..a627a873 100644 --- a/app/src/main/java/otus/homework/coroutines/MainActivity.kt +++ b/app/src/main/java/otus/homework/coroutines/MainActivity.kt @@ -2,12 +2,22 @@ package otus.homework.coroutines import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.lifecycle.coroutineScope +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.concurrent.Executors class MainActivity : AppCompatActivity() { - lateinit var catsPresenter: CatsPresenter + private lateinit var catsPresenter: CatsPresenter - private val diContainer = DiContainer() + private val diContainer = DiContainer(lifecycle) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -15,7 +25,7 @@ class MainActivity : AppCompatActivity() { val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView setContentView(view) - catsPresenter = CatsPresenter(diContainer.service) + catsPresenter = CatsPresenter(diContainer.service, diContainer.presenterScope) view.presenter = catsPresenter catsPresenter.attachView(view) catsPresenter.onInitComplete() @@ -23,8 +33,9 @@ class MainActivity : AppCompatActivity() { override fun onStop() { if (isFinishing) { + catsPresenter.cancelJob() catsPresenter.detachView() } super.onStop() } -} \ No newline at end of file +} From e44ae74e2d6bf0f6e3d087bdeeea08fe718bcc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=90=D0=B3=D0=B0=D1=84=D0=BE?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Mon, 20 Jan 2025 23:08:47 +0300 Subject: [PATCH 2/3] =?UTF-8?q?2.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BA=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=83=20=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=20=D1=80=D0=B0=D0=BD=D0=B4=D0=BE=D0=BC?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../otus/homework/coroutines/CatsPresenter.kt | 54 ++++++++++++------- .../otus/homework/coroutines/CatsService.kt | 2 +- .../java/otus/homework/coroutines/CatsView.kt | 9 ++-- .../otus/homework/coroutines/DataModel.kt | 6 +++ .../otus/homework/coroutines/DiContainer.kt | 13 ++++- .../main/java/otus/homework/coroutines/Dto.kt | 12 +++++ .../java/otus/homework/coroutines/Fact.kt | 10 ---- .../otus/homework/coroutines/ImageService.kt | 10 ++++ .../otus/homework/coroutines/MainActivity.kt | 9 +++- app/src/main/res/layout/activity_main.xml | 42 +++++++++------ 10 files changed, 116 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/otus/homework/coroutines/DataModel.kt create mode 100644 app/src/main/java/otus/homework/coroutines/Dto.kt delete mode 100644 app/src/main/java/otus/homework/coroutines/Fact.kt create mode 100644 app/src/main/java/otus/homework/coroutines/ImageService.kt diff --git a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt index 8d555188..cafaecf0 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt @@ -1,8 +1,10 @@ package otus.homework.coroutines +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.net.SocketTimeoutException @@ -10,32 +12,48 @@ import java.net.SocketTimeoutException class CatsPresenter( private val catsService: CatsService, + private val imageService: ImageService, private val presenterScope: CoroutineScope ) { private var _catsView: ICatsView? = null private var job: Job? = null + private val exceptionHandler by lazy { + CoroutineExceptionHandler { _, throwable -> + if (throwable is SocketTimeoutException) { + showToast("Не удалось получить ответ от сервером") + } else { + showToast(throwable.message ?: "No message") + CrashMonitor.trackWarning(throwable) + } + } + } + fun onInitComplete() { job?.cancel() - job = presenterScope.launch { - runCatching { catsService.getCatFact() } - .onSuccess { response -> - if (response.isSuccessful && response.body() != null) { - withContext(Dispatchers.Main) { _catsView?.populate(response.body()!!) } - } - } - .onFailure { t -> - if (t is SocketTimeoutException) { - withContext(Dispatchers.Main) { _catsView?.showToast("Не удалось получить ответ от сервером") } - } else { - withContext(Dispatchers.Main) { - _catsView?.showToast( - t.message ?: "No message" - ) - } - CrashMonitor.trackWarning(t) - } + job = presenterScope.launch(exceptionHandler) { + val factDeferred = async { catsService.getCatFact().body()?.fact } + val imageDeferred = async { imageService.getImage().body()?.getOrNull(0)?.url } + + val fact = factDeferred.await() + val image = imageDeferred.await() + + if (!fact.isNullOrEmpty() && !image.isNullOrEmpty()) { + withContext(Dispatchers.Main) { + _catsView?.populate( + DataModel( + fact.toString(), + image + ) + ) } + } + } + } + + private fun showToast(text: String) { + presenterScope.launch(Dispatchers.Main) { + _catsView?.showToast(text) } } diff --git a/app/src/main/java/otus/homework/coroutines/CatsService.kt b/app/src/main/java/otus/homework/coroutines/CatsService.kt index 89a65c3b..4748c78e 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsService.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsService.kt @@ -6,5 +6,5 @@ import retrofit2.http.GET interface CatsService { @GET("fact") - suspend fun getCatFact(): Response + suspend fun getCatFact(): Response } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsView.kt b/app/src/main/java/otus/homework/coroutines/CatsView.kt index 0d404f61..56d0c6db 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsView.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsView.kt @@ -3,9 +3,11 @@ 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, @@ -22,8 +24,9 @@ class CatsView @JvmOverloads constructor( } } - override fun populate(fact: Fact) { - findViewById(R.id.fact_textView).text = fact.fact + override fun populate(dataModel: DataModel) { + findViewById(R.id.fact_textView).text = dataModel.fact + Picasso.get().load(dataModel.imageUrl).into(findViewById(R.id.image)) } override fun showToast(text: String) { @@ -33,6 +36,6 @@ class CatsView @JvmOverloads constructor( interface ICatsView { - fun populate(fact: Fact) + fun populate(dataModel: DataModel) fun showToast(text: String) } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/DataModel.kt b/app/src/main/java/otus/homework/coroutines/DataModel.kt new file mode 100644 index 00000000..91763e75 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/DataModel.kt @@ -0,0 +1,6 @@ +package otus.homework.coroutines + +data class DataModel( + val fact: String, + val imageUrl: String +) \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/DiContainer.kt b/app/src/main/java/otus/homework/coroutines/DiContainer.kt index 6bf94ea8..b2193825 100644 --- a/app/src/main/java/otus/homework/coroutines/DiContainer.kt +++ b/app/src/main/java/otus/homework/coroutines/DiContainer.kt @@ -12,14 +12,23 @@ import java.util.concurrent.Executors class DiContainer(private val lifecycle: Lifecycle) { - private val retrofit by lazy { + private val catsRetrofit by lazy { Retrofit.Builder() .baseUrl("https://catfact.ninja/") .addConverterFactory(GsonConverterFactory.create()) .build() } - val service: CatsService by lazy { retrofit.create(CatsService::class.java) } + private val imageRetrofit by lazy { + Retrofit.Builder() + .baseUrl("https://api.thecatapi.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + val catsService: CatsService by lazy { catsRetrofit.create(CatsService::class.java) } + + val imageService: ImageService by lazy { imageRetrofit.create(ImageService::class.java) } val presenterScope by lazy { val mainDispatcher = Executors diff --git a/app/src/main/java/otus/homework/coroutines/Dto.kt b/app/src/main/java/otus/homework/coroutines/Dto.kt new file mode 100644 index 00000000..bf8ab5f2 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/Dto.kt @@ -0,0 +1,12 @@ +package otus.homework.coroutines + +import com.google.gson.annotations.SerializedName + +data class FactDto( + @SerializedName("fact") val fact: String, + @SerializedName("length") val length: Int +) + +data class ImageDto( + @SerializedName("url") var url: String +) \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/Fact.kt b/app/src/main/java/otus/homework/coroutines/Fact.kt deleted file mode 100644 index e30e2fc1..00000000 --- a/app/src/main/java/otus/homework/coroutines/Fact.kt +++ /dev/null @@ -1,10 +0,0 @@ -package otus.homework.coroutines - -import com.google.gson.annotations.SerializedName - -data class Fact( - @field:SerializedName("fact") - val fact: String, - @field:SerializedName("length") - val length: Int -) diff --git a/app/src/main/java/otus/homework/coroutines/ImageService.kt b/app/src/main/java/otus/homework/coroutines/ImageService.kt new file mode 100644 index 00000000..32bbc49c --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/ImageService.kt @@ -0,0 +1,10 @@ +package otus.homework.coroutines + +import retrofit2.Response +import retrofit2.http.GET + +interface ImageService { + + @GET("v1/images/search/") + suspend fun getImage(): Response> +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/MainActivity.kt b/app/src/main/java/otus/homework/coroutines/MainActivity.kt index a627a873..cd814068 100644 --- a/app/src/main/java/otus/homework/coroutines/MainActivity.kt +++ b/app/src/main/java/otus/homework/coroutines/MainActivity.kt @@ -5,7 +5,9 @@ import android.os.Bundle import androidx.lifecycle.coroutineScope import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.job @@ -25,7 +27,11 @@ class MainActivity : AppCompatActivity() { val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView setContentView(view) - catsPresenter = CatsPresenter(diContainer.service, diContainer.presenterScope) + catsPresenter = CatsPresenter( + diContainer.catsService, + diContainer.imageService, + diContainer.presenterScope + ) view.presenter = catsPresenter catsPresenter.attachView(view) catsPresenter.onInitComplete() @@ -35,6 +41,7 @@ class MainActivity : AppCompatActivity() { if (isFinishing) { catsPresenter.cancelJob() catsPresenter.detachView() + diContainer.presenterScope.coroutineContext.job.cancel("job cancel by activity's onStop()") } super.onStop() } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9508066d..08a2243e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,28 +3,38 @@ 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"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + +