Skip to content

Petr U. HW Coroutines #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: development
Choose a base branch
from
4 changes: 3 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ android {
compileSdkVersion 34
defaultConfig {
applicationId "otus.homework.coroutines"
minSdkVersion 23
minSdkVersion 24
targetSdkVersion 34
versionCode 1
versionName "1.0"
Expand All @@ -33,6 +33,8 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
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'
Expand Down
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Coroutines">
<activity android:name=".MainActivity"
<activity
android:name=".MainActivity2"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
10 changes: 10 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,10 @@
package otus.homework.coroutines

import com.google.gson.annotations.SerializedName

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

import retrofit2.http.GET

private const val VERSION_API = "v1/"

interface CatImageService {

@GET("${VERSION_API}images/search")
suspend fun getRandomImages(): List<CatImage>
}
70 changes: 52 additions & 18 deletions app/src/main/java/otus/homework/coroutines/CatsPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,69 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.SocketTimeoutException
import kotlin.coroutines.cancellation.CancellationException

interface ICatsPresenter {
fun onInitComplete()
fun attachView(catsView: ICatsView)
fun detachView()
}

class CatsPresenter(
private val catsService: CatsService
) {
private val catsService: CatsService,
private val catImageService: CatImageService,
): ICatsPresenter {

private var _catsView: ICatsView? = null
private val presenterScope = PresenterScope(CoroutineName("CatsCoroutine"))
private var workJob: Job? = null

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 onInitComplete() {
if (workJob?.isActive == true) {
_catsView?.handle(R.string.cats_wait_next_fact)
return
}
workJob = presenterScope.launch {
try {
withContext(Dispatchers.IO) {
val factDeferred = async {
catsService.getCatFact()
}
val imageDeferred = async {
catImageService.getRandomImages()
}
withContext(Dispatchers.Main) {
_catsView?.populate(
factDeferred.await(),
imageDeferred.await()
)
}
}
} catch (timeOutException: SocketTimeoutException) {
_catsView?.handle(R.string.app_request_timeout)
} catch (_: CancellationException) {
} catch (t: Throwable) {
CrashMonitor.trackError(t)
_catsView?.handle(t.message.toString())
}

override fun onFailure(call: Call<Fact>, t: Throwable) {
CrashMonitor.trackWarning()
}
})
}
}

fun attachView(catsView: ICatsView) {
override fun attachView(catsView: ICatsView) {
_catsView = catsView
}

fun detachView() {
override fun detachView() {
_catsView = null
CrashMonitor.trackWarning("Stop detachView")
presenterScope.cancel()
}
}
3 changes: 1 addition & 2 deletions app/src/main/java/otus/homework/coroutines/CatsService.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.http.GET

interface CatsService {

@GET("fact")
fun getCatFact() : Call<Fact>
suspend fun getCatFact(): Fact
}
64 changes: 59 additions & 5 deletions app/src/main/java/otus/homework/coroutines/CatsView.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,86 @@
package otus.homework.coroutines

import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat.startActivity
import androidx.core.os.bundleOf
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 presenter: ICatsPresenter? = null

override fun onFinishInflate() {
super.onFinishInflate()
findViewById<Button>(R.id.button).setOnClickListener {
presenter?.onInitComplete()
}
findViewById<Button>(R.id.button2).setOnClickListener {
// для проверки отмены корутин
val switchActivityIntent = Intent(context, MainActivity2::class.java)
startActivity(context, switchActivityIntent, bundleOf())
}
}

override fun populate(fact: Fact) {
override fun populate(fact: Fact, images: List<CatImage>) {
findViewById<TextView>(R.id.fact_textView).text = fact.fact
images.firstOrNull()?.let {
Picasso.get().load(it.url).into(findViewById<ImageView>(R.id.cat_image))
}
}

override fun populate(result: Result<CatData>) {
when (result) {
is Result.Success -> populate(result.data.fact, result.data.images)
is Result.Error -> {
when {
result.errorResId != null -> handle(result.errorResId)
result.errorMsg != null -> handle(result.errorMsg)
else -> handle(R.string.app_unknown_error)
}
}
}
}

override fun handle(@StringRes resId: Int) {
handle(context.getString(resId))
}

override fun handle(msg: String) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
}
}

interface ICatsView : IUserMessageHandler {

fun populate(fact: Fact, images: List<CatImage>)

fun populate(result: Result<CatData>)
}

interface ICatsView {
sealed class Result<out T> {

class Success<out T>(val data: T) : Result<T>()

class Error(
@StringRes val errorResId: Int? = null,
val errorMsg: String? = null
) : Result<Nothing>()
}

fun populate(fact: Fact)
}
data class CatData(
val fact: Fact,
val images: List<CatImage>,
)
71 changes: 71 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,71 @@
package otus.homework.coroutines

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.SocketTimeoutException
import kotlin.coroutines.cancellation.CancellationException

class CatsViewModel(
private val catsService: CatsService,
private val catImageService: CatImageService,
) : ViewModel(), ICatsPresenter {

private var _catsView: ICatsView? = null
private var workJob: Job? = null
private val scopeExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
val error = when (throwable) {
is SocketTimeoutException -> Result.Error(errorResId = R.string.app_request_timeout)
is CancellationException -> null
else -> Result.Error(errorMsg = throwable.message.toString())
}
error?.let { e ->
_catsView?.populate(e)
CrashMonitor.trackError(
throwable, "Handled by VMCoroutineExceptionHandler $coroutineContext"
)
}
}

override fun onInitComplete() {
if (workJob?.isActive == true) {
_catsView?.handle(R.string.cats_wait_next_fact)
return
}
workJob = viewModelScope.launch(scopeExceptionHandler) {
withContext(Dispatchers.IO) {
val factDeferred = async {
catsService.getCatFact()
}
val imageDeferred = async {
catImageService.getRandomImages()
}
withContext(Dispatchers.Main) {
_catsView?.populate(
Result.Success(
CatData(
factDeferred.await(),
imageDeferred.await()
)
)
)
}
}
}
}

override fun attachView(catsView: ICatsView) {
_catsView = catsView
}

override fun detachView() {
_catsView = null
viewModelScope.cancel()
}
}
11 changes: 10 additions & 1 deletion app/src/main/java/otus/homework/coroutines/CrashMonitor.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package otus.homework.coroutines

import android.util.Log

object CrashMonitor {

/**
* Pretend this is Crashlytics/AppCenter
*/
fun trackWarning() {
fun trackWarning(msg: String) {
Log.w(MONITOR_TAG, msg)
}

fun trackError(t: Throwable? = null, msg: String? = null) {
Log.e(MONITOR_TAG, msg, t)
}

private const val MONITOR_TAG = "OHC_CrashMonitor"
}
15 changes: 15 additions & 0 deletions app/src/main/java/otus/homework/coroutines/DiContainer.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
package otus.homework.coroutines

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

class DiContainer {

private val retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://catfact.ninja/")
.client(OkHttpClient.Builder().apply {
//для проверки работы при SocketTimeoutException
readTimeout(5, TimeUnit.SECONDS)
}.build())
.addConverterFactory(GsonConverterFactory.create())
.build()
}

private val retrofitImage by lazy {
Retrofit.Builder()
.baseUrl("https://api.thecatapi.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val service by lazy { retrofit.create(CatsService::class.java) }

val imageServise by lazy { retrofitImage.create(CatImageService::class.java) }
}
10 changes: 10 additions & 0 deletions app/src/main/java/otus/homework/coroutines/IUserMessageHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package otus.homework.coroutines

import androidx.annotation.StringRes

interface IUserMessageHandler {

fun handle(@StringRes resId: Int)

fun handle(msg: String)
}
Loading