Skip to content

refactor() coroutines homework #OTUS_Android-prof-2024-12 #255

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 14 commits into
base: development
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
}
53 changes: 39 additions & 14 deletions app/src/main/java/otus/homework/coroutines/CatsPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException

class CatsPresenter(
private val catsService: CatsService
private val catsService: CatsService,
private val imageService: ImageService,
private val presenterScope: CoroutineScope
) {

private var _catsView: ICatsView? = null
private var job: 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()!!)
}
job = presenterScope.async {
val image = async { imageService.getCatImage().first() }
val fact = async { catsService.getCatFact() }

_catsView?.populate(CatsUIState(fact.await(), image.await()))
}
presenterScope.launch {
try {
(job as Deferred<*>).await()
} catch (e: CancellationException) {
throw e
} catch (e: SocketTimeoutException) {
showToast("Не удалось получить ответ от сервера")
} catch (e: Exception) {
CrashMonitor.trackWarning(e)
e.message?.let(::showToast)
}
}
}

override fun onFailure(call: Call<Fact>, t: Throwable) {
CrashMonitor.trackWarning()
}
})
private fun showToast(message: String) {
_catsView?.showToast(message)
}

fun attachView(catsView: ICatsView) {
Expand All @@ -32,4 +49,12 @@ class CatsPresenter(
fun detachView() {
_catsView = null
}

fun cancelCoroutine() {
job?.run {
if (isActive) {
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
}
6 changes: 6 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CatsUIState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

data class CatsUIState(
val fact: Fact,
val image: Image
)
18 changes: 14 additions & 4 deletions app/src/main/java/otus/homework/coroutines/CatsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ 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,
private val context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView {
Expand All @@ -21,12 +24,19 @@ class CatsView @JvmOverloads constructor(
}
}

override fun populate(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.fact
override fun populate(state: CatsUIState) {
findViewById<TextView>(R.id.fact_textView).text = state.fact.fact
Picasso.get().load(state.image.url).into(findViewById<ImageView>(R.id.catImage))
}

override fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}

interface ICatsView {

fun populate(fact: Fact)
fun populate(state: CatsUIState)

fun showToast(message: String)
}
68 changes: 68 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,68 @@
package otus.homework.coroutines

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException

class CatsViewModel(
private val catsService: CatsService,
private val imageService: ImageService
) : ViewModel() {

private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
when(throwable) {
is SocketTimeoutException -> _uiState.tryEmit(Result.Error(Throwable("Не удалось получить ответ от сервера")))
else -> {
CrashMonitor.trackWarning(throwable)
_uiState.tryEmit(Result.Error(throwable))
}
}
}

private val _uiState = MutableStateFlow<Result?>(null)
val uiState = _uiState.onStart { load() }
.filterNotNull()
.shareIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
replay = 1
)

fun load() {
viewModelScope.launch(exceptionHandler) {
val fact = async { catsService.getCatFact() }
val image = async { imageService.getCatImage() }

_uiState.emit(
Result.Success(
CatsUIState(
fact = fact.await(),
image = image.await()[0]
)
)
)
}
}

companion object {
fun provideFactory(
catsService: CatsService,
imageService: ImageService
): ViewModelProvider.Factory = viewModelFactory {
initializer {
CatsViewModel(catsService, imageService)
}
}
}
}
3 changes: 1 addition & 2 deletions app/src/main/java/otus/homework/coroutines/CrashMonitor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ object CrashMonitor {
/**
* Pretend this is Crashlytics/AppCenter
*/
fun trackWarning() {
}
fun trackWarning(exception: Throwable) {}
}
24 changes: 19 additions & 5 deletions app/src/main/java/otus/homework/coroutines/DiContainer.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package otus.homework.coroutines

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class DiContainer {

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

private fun buildRetrofit(baseUrl: String) =
Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(converter)
.build()

val catsService by lazy {
buildRetrofit("https://catfact.ninja/").create(
CatsService::class.java
)
}

val imageService by lazy {
buildRetrofit("https://api.thecatapi.com/v1/images/").create(
ImageService::class.java
)
}

val service by lazy { retrofit.create(CatsService::class.java) }
val presenterScope get() = CoroutineScope(CoroutineName("CatsCoroutine") + Dispatchers.Main)
}
10 changes: 10 additions & 0 deletions app/src/main/java/otus/homework/coroutines/Image.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 Image(
@field:SerializedName("id")
val id: String,
@field:SerializedName("url")
val url: String
)
8 changes: 8 additions & 0 deletions app/src/main/java/otus/homework/coroutines/ImageService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package otus.homework.coroutines

import retrofit2.http.GET

interface ImageService {
@GET("search")
suspend fun getCatImage(): List<Image>
}
65 changes: 52 additions & 13 deletions app/src/main/java/otus/homework/coroutines/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
package otus.homework.coroutines

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.squareup.picasso.Picasso
import kotlinx.coroutines.launch
import otus.homework.coroutines.Result.*

class MainActivity : AppCompatActivity() {

lateinit var catsPresenter: CatsPresenter

private val diContainer = DiContainer()

private lateinit var fact: TextView
private lateinit var image: ImageView
private lateinit var loadButton: Button

private val viewModel by lazy {
ViewModelProvider(
this, CatsViewModel.provideFactory(diContainer.catsService, diContainer.imageService)
)[CatsViewModel::class.java]
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView
val view = layoutInflater.inflate(R.layout.activity_main, null)
setContentView(view)
fact = view.findViewById(R.id.fact_textView)
image = view.findViewById(R.id.catImage)
loadButton = view.findViewById(R.id.button)

processState()
setListeners()
}

catsPresenter = CatsPresenter(diContainer.service)
view.presenter = catsPresenter
catsPresenter.attachView(view)
catsPresenter.onInitComplete()
private fun setListeners() {
loadButton.setOnClickListener {
viewModel.load()
}
}

override fun onStop() {
if (isFinishing) {
catsPresenter.detachView()
private fun processState() {
lifecycleScope.launch {
viewModel.uiState.collect { state ->
when(state) {
is Success<*> -> {
(state.data as CatsUIState).also {
fact.text = it.fact.fact
Picasso.get().load(it.image.url).into(image)
}
}
is Error -> {
showToast(state.error)
viewModel.load()
}
}
}
}
super.onStop()
}

private fun showToast(throwable: Throwable, duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, throwable.message, duration).show()
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/otus/homework/coroutines/Result.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

sealed class Result{
data class Success<out T>(val data: T) : Result()
data class Error(val error: Throwable) : Result()
}
14 changes: 12 additions & 2 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageView
android:id="@+id/catImage"
android:layout_width="300dp"
android:layout_height="300dp"
android:contentDescription="@string/cat_image_description"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/fact_textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>

<TextView
android:id="@+id/fact_textView"
android:textColor="@color/black"
android:textSize="24sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/catImage" />

<Button
android:id="@+id/button"
Expand Down
Loading