From 566de5106d148fc81a964d750eba571b11d12859 Mon Sep 17 00:00:00 2001 From: Aleksei Vinogradov Date: Mon, 15 Jul 2024 23:18:49 +0700 Subject: [PATCH 1/8] =?UTF-8?q?=D0=94=D0=BE=D0=BC=D0=B0=D1=88=D0=BD=D1=8F?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 2 + .../main/java/otus/homework/coroutines/Cat.kt | 6 + .../{CatsService.kt => CatFactService.kt} | 5 +- .../homework/coroutines/CatImageService.kt | 9 ++ .../coroutines/CatsCoroutineContext.kt | 16 +++ .../otus/homework/coroutines/CatsPresenter.kt | 92 ++++++++++-- .../java/otus/homework/coroutines/CatsView.kt | 61 +++++++- .../otus/homework/coroutines/CatsViewModel.kt | 132 ++++++++++++++++++ .../otus/homework/coroutines/CrashMonitor.kt | 7 +- .../otus/homework/coroutines/DiContainer.kt | 22 ++- .../java/otus/homework/coroutines/Fact.kt | 10 -- .../otus/homework/coroutines/FactResponse.kt | 10 ++ .../otus/homework/coroutines/ImageResponse.kt | 14 ++ .../otus/homework/coroutines/MainActivity.kt | 76 ++++++++-- .../homework/coroutines/PresenterScope.kt | 14 ++ .../java/otus/homework/coroutines/Result.kt | 10 ++ .../coroutines/ServerResponseToCatUiMapper.kt | 4 + .../otus/homework/coroutines/Typealias.kt | 3 + app/src/main/res/layout/activity_main.xml | 30 +++- app/src/main/res/values/strings.xml | 2 + 20 files changed, 479 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/otus/homework/coroutines/Cat.kt rename app/src/main/java/otus/homework/coroutines/{CatsService.kt => CatFactService.kt} (50%) create mode 100644 app/src/main/java/otus/homework/coroutines/CatImageService.kt create mode 100644 app/src/main/java/otus/homework/coroutines/CatsCoroutineContext.kt create mode 100644 app/src/main/java/otus/homework/coroutines/CatsViewModel.kt delete mode 100644 app/src/main/java/otus/homework/coroutines/Fact.kt create mode 100644 app/src/main/java/otus/homework/coroutines/FactResponse.kt create mode 100644 app/src/main/java/otus/homework/coroutines/ImageResponse.kt create mode 100644 app/src/main/java/otus/homework/coroutines/PresenterScope.kt create mode 100644 app/src/main/java/otus/homework/coroutines/Result.kt create mode 100644 app/src/main/java/otus/homework/coroutines/ServerResponseToCatUiMapper.kt create mode 100644 app/src/main/java/otus/homework/coroutines/Typealias.kt diff --git a/app/build.gradle b/app/build.gradle index a414e0e8..f61d978f 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.8.3' + implementation 'androidx.fragment:fragment-ktx:1.8.1' } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/Cat.kt b/app/src/main/java/otus/homework/coroutines/Cat.kt new file mode 100644 index 00000000..82063859 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/Cat.kt @@ -0,0 +1,6 @@ +package otus.homework.coroutines + +data class Cat( + val fact: String, + val imageUrl: String +) \ 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/CatFactService.kt similarity index 50% rename from app/src/main/java/otus/homework/coroutines/CatsService.kt rename to app/src/main/java/otus/homework/coroutines/CatFactService.kt index 479b2cfb..92683341 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsService.kt +++ b/app/src/main/java/otus/homework/coroutines/CatFactService.kt @@ -1,10 +1,9 @@ package otus.homework.coroutines -import retrofit2.Call import retrofit2.http.GET -interface CatsService { +interface CatFactService { @GET("fact") - fun getCatFact() : Call + suspend fun getCatFact(): FactResponse? } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatImageService.kt b/app/src/main/java/otus/homework/coroutines/CatImageService.kt new file mode 100644 index 00000000..9cd2930d --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatImageService.kt @@ -0,0 +1,9 @@ +package otus.homework.coroutines + +import retrofit2.http.GET + +interface CatImageService { + + @GET("images/search") + suspend fun getCatImage(): List? +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsCoroutineContext.kt b/app/src/main/java/otus/homework/coroutines/CatsCoroutineContext.kt new file mode 100644 index 00000000..5a4d4611 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatsCoroutineContext.kt @@ -0,0 +1,16 @@ +package otus.homework.coroutines + +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName + +/** + * Вынесено отдельно, так как используется, как в презентере, так и во ViewModel + */ + +fun getCatsExceptionHandler() = + CoroutineExceptionHandler { _, e -> + CrashMonitor.trackWarning(e) + } + +fun getCatsCoroutineName() = + CoroutineName("CatsCoroutine") \ 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..e1f27ea1 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt @@ -1,28 +1,95 @@ package otus.homework.coroutines -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import java.net.SocketTimeoutException + +private const val PRESENTER_CAT_JOB_KEY = "CatJob" +private const val PRESENTER_AWAIT_TIMEOUT = 20000L class CatsPresenter( - private val catsService: CatsService + private val catFactService: CatFactService, + private val catImageService: CatImageService, + private val scope: CoroutineScope = PresenterScope(), + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, + private val jobs: MutableMap = mutableMapOf(), + /** + * Должно быть только параметром, не переменной. + * Локальная переменная создается дальше, чтобы ее можно было занулить + */ + isDataLoadedCallbaсk: (Boolean) -> Unit ) { private var _catsView: ICatsView? = null + /** + * Локальная копия коллбэка для того, + * чтобы занулить ее при необходимости + * и минимизировать шанс утечки + * */ + private var isDataLoadedCallback: ((Boolean) -> Unit)? = isDataLoadedCallbaсk + fun onInitComplete() { - catsService.getCatFact().enqueue(object : Callback { + /** Если корутина уже запущена, то ничего не делаем */ + if (jobs.getOrDefault(PRESENTER_CAT_JOB_KEY, null)?.isActive == true) return - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - _catsView?.populate(response.body()!!) - } + /** Добавляем Job в мапу по ключу */ + jobs[PRESENTER_CAT_JOB_KEY] = scope.launch { + val factResponseDeffered = scope.async(ioDispatcher) { + catFactService.getCatFact() } + val imageResponseDeffered = scope.async(ioDispatcher) { + catImageService.getCatImage() + } + + /** Timeout, чтобы не ждать ответа бесконечно */ + withTimeout(PRESENTER_AWAIT_TIMEOUT) { + val result = try { + /** Тестовое пробрасывание SocketTimeoutException для проверки */ +// throw SocketTimeoutException() + + val factResponse = factResponseDeffered.await() + ?: return@withTimeout + val imageResponse = imageResponseDeffered.await() + ?: return@withTimeout + val imageResponseFirstElement = imageResponse.firstOrNull() + ?: return@withTimeout - override fun onFailure(call: Call, t: Throwable) { - CrashMonitor.trackWarning() + val cat = mapServerResponseToCat( + factResponse = factResponse, + imageResponse = imageResponseFirstElement + ) + + Result.Success(cat) + } catch (e: CancellationException) { + throw e + } catch (e: SocketTimeoutException) { + Result.Error.SocketError + } catch (e: Throwable) { + CrashMonitor.trackWarning(e) + Result.Error.OtherError(e) + } + + /** + * Логика показа Тоста внутри View, + * в зависимости от результата + */ + _catsView?.populate(result) + isDataLoadedCallback?.invoke(true) } - }) + } + } + + fun cancelAllCoroutines() { + scope.coroutineContext.cancelChildren() + jobs.clear() } fun attachView(catsView: ICatsView) { @@ -31,5 +98,6 @@ class CatsPresenter( fun detachView() { _catsView = null + isDataLoadedCallback = null } } \ 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..b5388b69 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsView.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsView.kt @@ -2,31 +2,84 @@ package otus.homework.coroutines import android.content.Context import android.util.AttributeSet +import android.util.TypedValue 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 +/** + * Считаю, что методы во вью не нужно делать suspend. + * Если ответ от сервера получен, то нет смысла создавать точку приостановки в данных функциях, + * это лишняя генерация кода под капотом в suspend функциях. + */ class CatsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView { - var presenter :CatsPresenter? = null + var presenter: CatsPresenter? = null + + var viewModel: CatsViewModel? = null override fun onFinishInflate() { super.onFinishInflate() findViewById