Skip to content

Aleksei Vinogradov - Coroutines #215

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 8 commits into
base: development
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
6 changes: 6 additions & 0 deletions app/src/main/java/otus/homework/coroutines/Cat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

data class Cat(
val fact: String,
val imageUrl: String
)
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 {
interface CatFactService {

@GET("fact")
fun getCatFact() : Call<Fact>
suspend fun getCatFact(): FactResponse
}
9 changes: 9 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,9 @@
package otus.homework.coroutines

import retrofit2.http.GET

interface CatImageService {

@GET("images/search")
suspend fun getCatImage(): List<ImageResponse>
}
19 changes: 19 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CatsCoroutineContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package otus.homework.coroutines

import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlin.coroutines.CoroutineContext

/**
* Вынесено отдельно, так как используется, как в презентере, так и во ViewModel
*/

fun getCoroutineContext(): CoroutineContext = getCatsExceptionHandler() + getCatsCoroutineName()

fun getCatsExceptionHandler() =
CoroutineExceptionHandler { _, e ->
CrashMonitor.trackWarning(e)
}

fun getCatsCoroutineName() =
CoroutineName("CatsCoroutine")
63 changes: 50 additions & 13 deletions app/src/main/java/otus/homework/coroutines/CatsPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,65 @@
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.async
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException

private const val PRESENTER_CAT_JOB_KEY = "CatJob"

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 isDataLoadedCallbaсk: (Boolean) -> Unit
) {

private var _catsView: ICatsView? = 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()!!)
}
scope.launch {
val factResponseDeffered = scope.async(ioDispatcher) {
catFactService.getCatFact()
}
val imageResponseDeffered = scope.async(ioDispatcher) {
catImageService.getCatImage()
}

val result = try {
/** Тестовое пробрасывание SocketTimeoutException для проверки */
// throw SocketTimeoutException()

override fun onFailure(call: Call<Fact>, t: Throwable) {
CrashMonitor.trackWarning()
val factResponse = factResponseDeffered.await()
val imageResponse = imageResponseDeffered.await()
val imageResponseFirstElement = imageResponse.firstOrNull()
?: return@launch

val cat = mapServerResponseToCat(
factResponse = factResponse,
imageResponse = imageResponseFirstElement
)

Result.Success<Cat>(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)
isDataLoadedCallbaсk(true)
}
}

fun attachView(catsView: ICatsView) {
Expand Down
61 changes: 57 additions & 4 deletions app/src/main/java/otus/homework/coroutines/CatsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Button>(R.id.button).setOnClickListener {
presenter?.onInitComplete()
viewModel?.onInitComplete()
}
}

override fun populate(result: Result) {
when (result) {
is Result.Success<*> -> {
val cat = result.body as Cat
setImage(
imageUrl = cat.imageUrl,
imageView = findViewById<ImageView>(R.id.image)
)
findViewById<TextView>(R.id.fact_textView).text = cat.fact
}

Result.Error.SocketError -> {
val errorMessage = context.getString(R.string.no_internet_connection)
showToast(errorMessage)
}

is Result.Error.OtherError -> {
val errorMessage = result.e.message
?: context.getString(R.string.unknown_error)
showToast(errorMessage)
}
}
}

override fun populate(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.fact
private fun setImage(imageUrl: String, imageView: ImageView) {
val requiredSizeInPixels = dpToPixels(400F)
Picasso.get()
.load(imageUrl)
.resize(requiredSizeInPixels, requiredSizeInPixels)
.centerInside()
.into(imageView)
}

private fun dpToPixels(dp: Float): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
context.resources.displayMetrics
).toInt()
}

private fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}

interface ICatsView {

fun populate(fact: Fact)
fun populate(result: Result)
}
89 changes: 89 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,89 @@
package otus.homework.coroutines

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException

class CatsViewModel(
private val catFactService: CatFactService,
private val catImageService: CatImageService,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val isDataLoadedCallbaсk: (Boolean) -> Unit
) : ViewModel() {

private var _catsView: ICatsView? = null
private var isLogsEnabled = true

fun onInitComplete() {
viewModelScope.launch(getCoroutineContext()) {
val factResponseDeffered =
viewModelScope.async(ioDispatcher) {
catFactService.getCatFact()
}
val imageResponseDeffered =
viewModelScope.async(ioDispatcher) {
catImageService.getCatImage()
}

val result = try {
/** Тестовое пробрасывание SocketTimeoutException для проверки */
// throw SocketTimeoutException()

val factResponse = factResponseDeffered.await()
val imageResponse = imageResponseDeffered.await()
val imageResponseFirstElement = imageResponse.firstOrNull()
?: return@launch

val cat = mapServerResponseToCat(
factResponse = factResponse,
imageResponse = imageResponseFirstElement
)

Result.Success<Cat>(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)
isDataLoadedCallbaсk(true)
}
}

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

fun detachView() {
_catsView = null
}
}

class CatsViewModelFactory(
private val catFactService: CatFactService,
private val catImageService: CatImageService,
private val isDataLoadedCallbaсk: (Boolean) -> Unit
) :
ViewModelProvider.NewInstanceFactory() {

@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = CatsViewModel(
catFactService = catFactService,
catImageService = catImageService,
isDataLoadedCallbaсk = isDataLoadedCallbaсk,
) as T
}
7 changes: 6 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,15 @@
package otus.homework.coroutines

import android.util.Log

object CrashMonitor {

private const val CRASH_MONITOR_TAG = "CoroutinesHomework: CrashMonitor"

/**
* Pretend this is Crashlytics/AppCenter
*/
fun trackWarning() {
fun trackWarning(e: Throwable) {
Log.d(CRASH_MONITOR_TAG, "trackWarning(): $e")
}
}
22 changes: 19 additions & 3 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,28 @@ import retrofit2.converter.gson.GsonConverterFactory

class DiContainer {

private val retrofit by lazy {
val catFactService by lazy {
catFactRetrofit.create(CatFactService::class.java)
}

val catImageService by lazy {
catImageRetrofit.create(CatImageService::class.java)
}

private val serviceBuilder by lazy {
Retrofit.Builder()
.baseUrl("https://catfact.ninja/")
.addConverterFactory(GsonConverterFactory.create())
}

private val catFactRetrofit by lazy {
serviceBuilder
.baseUrl("https://catfact.ninja/")
.build()
}

val service by lazy { retrofit.create(CatsService::class.java) }
private val catImageRetrofit by lazy {
serviceBuilder
.baseUrl("https://api.thecatapi.com/v1/")
.build()
}
}
10 changes: 0 additions & 10 deletions app/src/main/java/otus/homework/coroutines/Fact.kt

This file was deleted.

10 changes: 10 additions & 0 deletions app/src/main/java/otus/homework/coroutines/FactResponse.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 FactResponse(
@field:SerializedName("fact")
val fact: String,
@field:SerializedName("length")
val length: Int
)
Loading