Skip to content
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
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ 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.7.0")
implementation("androidx.activity:activity-ktx:1.8.2")
}
58 changes: 46 additions & 12 deletions app/src/main/java/otus/homework/coroutines/CatsPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException
import kotlin.coroutines.CoroutineContext

class CatsPresenter(
private val catsService: CatsService
private val catsService: CatsService,
private val catsImageService: CatsImageService
) {

private var _catsView: ICatsView? = null

private val presenterScope = PresenterScope()

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()!!)
presenterScope.launch {
try {
val factDeferred = async(Dispatchers.IO) {
catsService.getCatFact()
}
val imageDeferred = async(Dispatchers.IO) {
catsImageService.getCatImage()
}
}

override fun onFailure(call: Call<Fact>, t: Throwable) {
CrashMonitor.trackWarning()
val fact = factDeferred.await()
val image = imageDeferred.await()

val presentationModel = PresentationModel(fact = fact.fact, imageUrl = image[0].url)
_catsView?.populate(presentationModel)

} catch (_: SocketTimeoutException) {
_catsView?.showToast("Не удалось получить ответ от сервера")
} catch (e: Exception) {
CrashMonitor.trackWarning(e)
_catsView?.showToast(e.message ?: "")
}
})
}
}

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

fun cancelJob() {
presenterScope.cancel()
}
}

class PresenterScope() : CoroutineScope {
private val job = Job()

override val coroutineContext: CoroutineContext =
Dispatchers.Main + job + CoroutineName("CatsCoroutine")

fun cancel() {
job.cancel()
}
}
9 changes: 6 additions & 3 deletions app/src/main/java/otus/homework/coroutines/CatsService.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.http.GET

interface CatsService {

@GET("fact")
fun getCatFact() : Call<Fact>
suspend fun getCatFact(): Fact
}

interface CatsImageService {
@GET("v1/images/search")
suspend fun getCatImage(): List<Image>
}
31 changes: 25 additions & 6 deletions app/src/main/java/otus/homework/coroutines/CatsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,49 @@ 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,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView {
) : ConstraintLayout(context, attrs, defStyleAttr) {

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(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.fact
fun populate(presentationModel: PresentationModel) {
findViewById<TextView>(R.id.fact_textView).text = presentationModel.fact
Picasso
.get()
.load(presentationModel.imageUrl)
.into(findViewById<ImageView>(R.id.cat_imageView))
}

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

interface ICatsView {

fun populate(fact: Fact)
fun populate(presentationModel: PresentationModel)

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

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException

class CatsViewModel(
private val catsService: CatsService,
private val catsImageService: CatsImageService
) : ViewModel() {

private val _presentationState = MutableStateFlow<Result<PresentationModel>?>(null)
val presentationState = _presentationState.asStateFlow()

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
CrashMonitor.trackWarning(exception as Exception)
_presentationState.value = Result.Error(exception.message ?: "")
}

fun onInitComplete() {
viewModelScope.launch(exceptionHandler) {
try {
val factDeferred = async(Dispatchers.IO) {
catsService.getCatFact()
}
val imageDeferred = async(Dispatchers.IO) {
catsImageService.getCatImage()
}

val fact = factDeferred.await()
val image = imageDeferred.await()

_presentationState.value = Result.Success(
PresentationModel(fact = fact.fact, imageUrl = image[0].url)
)

} catch (_: SocketTimeoutException) {
_presentationState.value = Result.Error(
"Не удалось получить ответ от сервера"
)
}
}
}
}

class CatsViewModelFactory(
private val catsService: CatsService,
private val catsImageService: CatsImageService
) :
ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
if (modelClass.isAssignableFrom(CatsViewModel::class.java)) {
return CatsViewModel(catsService, catsImageService) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
3 changes: 2 additions & 1 deletion app/src/main/java/otus/homework/coroutines/CrashMonitor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ object CrashMonitor {
/**
* Pretend this is Crashlytics/AppCenter
*/
fun trackWarning() {
fun trackWarning(e: Exception) {
//Firebase.crashlytics.log("e")
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/otus/homework/coroutines/DiContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ class DiContainer {
}

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

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

val imageService by lazy { imageRetrofit.create(CatsImageService::class.java) }
}
8 changes: 8 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,8 @@
package otus.homework.coroutines

data class Image(
val id: String,
val url: String,
val width: Int,
val height: Int
)
37 changes: 26 additions & 11 deletions app/src/main/java/otus/homework/coroutines/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,44 @@ package otus.homework.coroutines

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

lateinit var catsPresenter: CatsPresenter

private val diContainer = DiContainer()

private val viewModel: CatsViewModel by viewModels {
CatsViewModelFactory(
catsService = diContainer.service,
catsImageService = diContainer.imageService
)
}

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

val view = layoutInflater.inflate(R.layout.activity_main, null) as CatsView
setContentView(view)

catsPresenter = CatsPresenter(diContainer.service)
view.presenter = catsPresenter
catsPresenter.attachView(view)
catsPresenter.onInitComplete()
}
view.viewModel = viewModel
viewModel.onInitComplete()

lifecycleScope.launch {
viewModel.presentationState.collect { result ->
when (result) {
is Result.Success -> {
view.populate(result.data)
}

is Result.Error -> {
view.showToast(result.message)
}

override fun onStop() {
if (isFinishing) {
catsPresenter.detachView()
else -> {}
}
}
}
super.onStop()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

data class PresentationModel(
val fact: String,
val imageUrl: String
)
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<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}
15 changes: 13 additions & 2 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,33 @@
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageView
android:id="@+id/cat_imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/cat_s_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/fact_textView"/>

<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_constraintBottom_toTopOf="@id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/cat_imageView" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/more_facts"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/fact_textView" />
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<resources>
<string name="app_name">Cat Facts </string>
<string name="more_facts">More Facts</string>
<string name="cat_s_image">Cat\'s image</string>
</resources>