From a4372bb370f2042454d983e1ce0ed054e65f129a Mon Sep 17 00:00:00 2001 From: Taranov Dmitriy Date: Tue, 20 Jan 2026 23:48:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=97=20Kotlin=20coroutines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 2 + .../coroutines/CatPresentationModel.kt | 7 ++ .../otus/homework/coroutines/CatsPresenter.kt | 72 ++++++++++++++-- .../otus/homework/coroutines/CatsService.kt | 13 ++- .../java/otus/homework/coroutines/CatsView.kt | 22 ++++- .../otus/homework/coroutines/CatsViewModel.kt | 83 +++++++++++++++++++ .../otus/homework/coroutines/DiContainer.kt | 12 ++- .../java/otus/homework/coroutines/Fact.kt | 10 ++- .../otus/homework/coroutines/MainActivity.kt | 50 ++++++++--- .../java/otus/homework/coroutines/Result.kt | 7 ++ 10 files changed, 253 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/otus/homework/coroutines/CatPresentationModel.kt create mode 100644 app/src/main/java/otus/homework/coroutines/CatsViewModel.kt create mode 100644 app/src/main/java/otus/homework/coroutines/Result.kt diff --git a/app/build.gradle b/app/build.gradle index a414e0e8..f5dd6086 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,4 +41,6 @@ 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 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.room:room-runtime-android:2.8.4' } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatPresentationModel.kt b/app/src/main/java/otus/homework/coroutines/CatPresentationModel.kt new file mode 100644 index 00000000..cb5faee7 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatPresentationModel.kt @@ -0,0 +1,7 @@ +package otus.homework.coroutines + +data class CatPresentationModel( + val fact: String, + val imageUrl: String +) + diff --git a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt index e4b05120..d24f65ad 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt @@ -1,28 +1,74 @@ package otus.homework.coroutines +import kotlinx.coroutines.* +import java.net.SocketTimeoutException + import retrofit2.Call import retrofit2.Callback import retrofit2.Response class CatsPresenter( - private val catsService: CatsService + private val catsService: CatsService, + private val catsImageService: CatsImageService ) { private var _catsView: ICatsView? = null + private var currentJob: Job? = null + + private val presenterScope = CoroutineScope( + Dispatchers.Main + SupervisorJob() + CoroutineName("CatsCoroutine") + ) fun onInitComplete() { - catsService.getCatFact().enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - _catsView?.populate(response.body()!!) + currentJob?.cancel() + + currentJob = presenterScope.launch { + try { + + _catsView?.showLoading(true) + + + val deferredFact = async(Dispatchers.IO) { + catsService.getCatFact() } - } - override fun onFailure(call: Call, t: Throwable) { - CrashMonitor.trackWarning() + val deferredImage = async(Dispatchers.IO) { + catsImageService.getCatImage() + } + + + val fact = deferredFact.await() + val image = deferredImage.await() + + val enhancedFact = Fact( + fact = fact.fact, + length = fact.length, + imageUrl = image.file // Добавляем URL картинки + ) + + // Передаем в существующий метод populate + _catsView?.populate(enhancedFact) + + _catsView?.showLoading(false) + + } catch (e: CancellationException) { + + _catsView?.showLoading(false) + } catch (e: SocketTimeoutException) { + _catsView?.showLoading(false) + handleError(e, "Не удалось получить ответ от сервера") + } catch (e: Exception) { + _catsView?.showLoading(false) + handleError(e, e.message ?: "Неизвестная ошибка") } - }) + } + } + + private fun handleError(e: Exception, message: String) { + CrashMonitor.trackWarning() + + _catsView?.showToast(message) } fun attachView(catsView: ICatsView) { @@ -32,4 +78,12 @@ class CatsPresenter( fun detachView() { _catsView = null } + + fun onStop() { + presenterScope.coroutineContext.cancelChildren() + } + + fun cancel() { + presenterScope.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..39dca162 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsService.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsService.kt @@ -6,5 +6,14 @@ import retrofit2.http.GET interface CatsService { @GET("fact") - fun getCatFact() : Call -} \ No newline at end of file + suspend fun getCatFact() : Fact +} + +interface CatsImageService { + @GET("meow") + suspend fun getCatImage(): CatImage +} + +data class CatImage( + val file: String +) \ 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..9ede7298 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsView.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsView.kt @@ -3,8 +3,12 @@ package otus.homework.coroutines import android.content.Context import android.util.AttributeSet import android.widget.Button +import android.widget.ImageView +import android.widget.ProgressBar import android.widget.TextView +import android.widget.Toast import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible class CatsView @JvmOverloads constructor( context: Context, @@ -12,7 +16,12 @@ class CatsView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView { - var presenter :CatsPresenter? = null + var presenter: CatsPresenter? = null + + private lateinit var factTextView: TextView + private lateinit var catImageView: ImageView + private lateinit var button: Button + private lateinit var progressBar: ProgressBar override fun onFinishInflate() { super.onFinishInflate() @@ -24,9 +33,20 @@ class CatsView @JvmOverloads constructor( override fun populate(fact: Fact) { findViewById(R.id.fact_textView).text = fact.fact } + + override fun showToast(message: String) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + + override fun showLoading(isLoading: Boolean) { + progressBar.isVisible = isLoading + button.isEnabled = !isLoading + } } interface ICatsView { fun populate(fact: Fact) + fun showToast(message: String) + fun showLoading(isLoading: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsViewModel.kt b/app/src/main/java/otus/homework/coroutines/CatsViewModel.kt new file mode 100644 index 00000000..7b359104 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatsViewModel.kt @@ -0,0 +1,83 @@ +package otus.homework.coroutines + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import java.net.SocketTimeoutException + +class CatsViewModel( + private val catsService: CatsService, + private val catsImageService: CatsImageService +) : ViewModel() { + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + + CrashMonitor.trackWarning() + + + _result.value = Result.Error( + message = when (throwable) { + is SocketTimeoutException -> "Не удалось получить ответ от сервера" + else -> throwable.message ?: "Неизвестная ошибка" + }, + exception = throwable + ) + } + + // LiveData для состояния + private val _result = MutableLiveData>() + val result: LiveData> = _result + + private var currentJob: Job? = null + + fun loadCatData() { + + currentJob?.cancel() + + currentJob = viewModelScope.launch(exceptionHandler) { + try { + + _result.value = Result.Loading + + + val deferredFact = async { catsService.getCatFact() } + val deferredImage = async { catsImageService.getCatImage() } + + + val (factResult, imageResult) = awaitAll(deferredFact, deferredImage) + + + val factWithImage = factResult.copy(imageUrl = imageResult.file) + + + _result.value = Result.Success(factWithImage) + + } catch (e: Exception) { + + if (e is SocketTimeoutException) { + _result.value = Result.Error("Не удалось получить ответ от сервера", e) + } else { + + _result.value = Result.Error(e.message ?: "Неизвестная ошибка", e) + } + } + } + } + + fun cancelCurrentRequest() { + currentJob?.cancel() + currentJob = null + } + + override fun onCleared() { + super.onCleared() + cancelCurrentRequest() + } +} + diff --git a/app/src/main/java/otus/homework/coroutines/DiContainer.kt b/app/src/main/java/otus/homework/coroutines/DiContainer.kt index 23ddc3b2..6e6abd54 100644 --- a/app/src/main/java/otus/homework/coroutines/DiContainer.kt +++ b/app/src/main/java/otus/homework/coroutines/DiContainer.kt @@ -12,5 +12,15 @@ class DiContainer { .build() } + private val retrofitCatsImage by lazy { + Retrofit.Builder() + .baseUrl("https://aws.random.cat/") // Базовый URL для картинок + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + val service by lazy { retrofit.create(CatsService::class.java) } -} \ No newline at end of file + + val imageService by lazy { retrofitCatsImage.create(CatsImageService::class.java) } +} + diff --git a/app/src/main/java/otus/homework/coroutines/Fact.kt b/app/src/main/java/otus/homework/coroutines/Fact.kt index 643a5a33..30ba5789 100644 --- a/app/src/main/java/otus/homework/coroutines/Fact.kt +++ b/app/src/main/java/otus/homework/coroutines/Fact.kt @@ -6,5 +6,11 @@ data class Fact( @field:SerializedName("fact") val fact: String, @field:SerializedName("length") - val length: Int -) \ No newline at end of file + val length: Int, + + val imageUrl: String? = null +){ + fun withImageUrl(imageUrl: String): Fact { + return Fact(fact, length, imageUrl) + } +} \ 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 a9dafb3b..c27ac6d1 100644 --- a/app/src/main/java/otus/homework/coroutines/MainActivity.kt +++ b/app/src/main/java/otus/homework/coroutines/MainActivity.kt @@ -2,29 +2,59 @@ package otus.homework.coroutines import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider class MainActivity : AppCompatActivity() { - lateinit var catsPresenter: CatsPresenter - + private lateinit var catsViewModel: CatsViewModel + private lateinit var catsView: CatsView private val diContainer = DiContainer() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView - setContentView(view) + catsView = layoutInflater.inflate(R.layout.activity_main, null) as CatsView + setContentView(catsView) + + + catsViewModel = ViewModelProvider( + this, + + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return CatsViewModel( + diContainer.service, + diContainer.imageService + ) as T + } + } + ).get(CatsViewModel::class.java) + + + catsViewModel.result.observe(this) { result -> + when (result) { + is Result.Loading -> { + // прогресс + } + is Result.Success -> { + catsView.populate(result.data) + } + is Result.Error -> { + catsView.showToast(result.message) + } + } + } - catsPresenter = CatsPresenter(diContainer.service) - view.presenter = catsPresenter - catsPresenter.attachView(view) - catsPresenter.onInitComplete() + catsViewModel.loadCatData() } + override fun onStop() { + super.onStop() if (isFinishing) { - catsPresenter.detachView() + // Очистка при необходимости } - super.onStop() } } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/Result.kt b/app/src/main/java/otus/homework/coroutines/Result.kt new file mode 100644 index 00000000..544d5997 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/Result.kt @@ -0,0 +1,7 @@ +package otus.homework.coroutines + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val message: String, val exception: Throwable? = null) : Result() + object Loading : Result() +}