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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.activity:activity-ktx:1.8.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.10'
Expand Down
8 changes: 8 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,8 @@
package otus.homework.coroutines

import com.google.gson.annotations.SerializedName

data class CatImage(
@field:SerializedName("file")
val file: String
)
8 changes: 8 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,8 @@
package otus.homework.coroutines

import retrofit2.http.GET

interface CatImageService {
@GET("meow")
suspend fun getRandomCatImage(): CatImage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package otus.homework.coroutines

data class CatPresentationData(
val fact: String,
val imageUrl: String
)
72 changes: 58 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,53 @@
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.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException
import kotlin.coroutines.cancellation.CancellationException

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

private var _catsView: ICatsView? = null
private val presenterScope = PresenterScope()
private var currentJob: Job? = null

fun onInitComplete() {
catsService.getCatFact().enqueue(object : Callback<Fact> {
currentJob?.cancel()
currentJob = presenterScope.launch {
try {
val factDeferred = async(Dispatchers.IO) { catsService.getCatFact() }
val imageDeferred = async(Dispatchers.IO) { catImageService.getRandomCatImage() }

override fun onResponse(call: Call<Fact>, response: Response<Fact>) {
if (response.isSuccessful && response.body() != null) {
_catsView?.populate(response.body()!!)
}
}
val fact = factDeferred.await()
val image = imageDeferred.await()

override fun onFailure(call: Call<Fact>, t: Throwable) {
CrashMonitor.trackWarning()
val presentationData = CatPresentationData(
fact = fact.fact,
imageUrl = image.file
)

_catsView?.populate(presentationData)
} catch (e: SocketTimeoutException) {
handleError("Не удалось получить ответ от сервером", e)
} catch (e: Exception) {
if (e is CancellationException) throw e
handleError(e.message ?: "Произошла ошибка", e)
}
})
}
}

private fun handleError(message: String, exception: Exception) {
presenterScope.launch {
_catsView?.showError(message)
CrashMonitor.logException(exception)
}
}

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

fun onStop() {
currentJob?.cancel()
currentJob = null
}

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

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

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

fun cancel() {
job.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
}
49 changes: 39 additions & 10 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,60 @@ package otus.homework.coroutines

import android.content.Context
import android.util.AttributeSet
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
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 {

var presenter :CatsPresenter? = null
private lateinit var factTextView: TextView
private lateinit var catImageView: ImageView
private lateinit var progressBar: ProgressBar
private lateinit var button: android.widget.Button

override fun onFinishInflate() {
super.onFinishInflate()
findViewById<Button>(R.id.button).setOnClickListener {
presenter?.onInitComplete()
}
factTextView = findViewById(R.id.fact_textView)
catImageView = findViewById(R.id.cat_imageView)
progressBar = findViewById(R.id.progress_bar)
button = findViewById(R.id.button)
}

override fun populate(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.fact
override fun populate(data: CatPresentationData) {
showContent()

factTextView.text = data.fact

Picasso.get()
.load(data.imageUrl)
.placeholder(android.R.drawable.ic_menu_gallery)
.error(android.R.drawable.ic_menu_report_image)
.into(catImageView)
}

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

interface ICatsView {
override fun showLoading() {
progressBar.visibility = VISIBLE
catImageView.visibility = GONE
factTextView.visibility = GONE
button.isEnabled = false
}

fun populate(fact: Fact)
private fun showContent() {
progressBar.visibility = GONE
catImageView.visibility = VISIBLE
factTextView.visibility = VISIBLE
button.isEnabled = true
}
}
60 changes: 60 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,60 @@
package otus.homework.coroutines

import androidx.lifecycle.ViewModel
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.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.net.SocketTimeoutException

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

private val _catsState =
MutableStateFlow<Result<CatPresentationData>>(Result.Loading)
val catsState: StateFlow<Result<CatPresentationData>> = _catsState.asStateFlow()

private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
viewModelScope.launch(Dispatchers.Main) {
val message = when (throwable) {
is SocketTimeoutException -> "Не удалось получить ответ от сервера"
else -> throwable.message ?: "Произошла ошибка"
}
_catsState.value = Result.Error(message, throwable as? Exception)
if (throwable is Exception) {
CrashMonitor.logException(throwable)
}
}
}

fun loadCatData() {
viewModelScope.launch(exceptionHandler) {
_catsState.value = Result.Loading

val factDeferred = async(Dispatchers.IO) { catsService.getCatFact() }
val imageDeferred = async(Dispatchers.IO) { catImageService.getRandomCatImage() }

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

val presentationData = CatPresentationData(
fact = fact.fact,
imageUrl = image.file
)

_catsState.value = Result.Success(presentationData)
}
}
}

sealed class Result<out T> {
object Loading : Result<Nothing>()
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val exception: Exception? = null) : Result<Nothing>()
}
7 changes: 7 additions & 0 deletions app/src/main/java/otus/homework/coroutines/CrashMonitor.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package otus.homework.coroutines

import android.util.Log

object CrashMonitor {

/**
* Pretend this is Crashlytics/AppCenter
*/
fun trackWarning() {
}

fun logException(exception: Exception) {
Log.wtf("Error", "Exception logged: ${exception.javaClass.simpleName} ${exception.message}")
exception.printStackTrace()
}
}
15 changes: 11 additions & 4 deletions app/src/main/java/otus/homework/coroutines/DiContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ package otus.homework.coroutines
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class DiContainer {

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

val service by lazy { retrofit.create(CatsService::class.java) }
private val catImageRetrofit by lazy {
Retrofit.Builder()
.baseUrl("https://aws.random.cat/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

val catFactService by lazy { catFactRetrofit.create(CatsService::class.java) }
val catImageService by lazy { catImageRetrofit.create(CatImageService::class.java) }
}
7 changes: 7 additions & 0 deletions app/src/main/java/otus/homework/coroutines/ICatsView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package otus.homework.coroutines

interface ICatsView {
fun populate(data: CatPresentationData)
fun showError(message: String)
fun showLoading()
}
51 changes: 39 additions & 12 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,56 @@ package otus.homework.coroutines

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

class MainActivity : AppCompatActivity() {

lateinit var catsPresenter: CatsPresenter
private lateinit var catsView: CatsView

private val diContainer = DiContainer()
private val catsViewModel: CatsViewModel by viewModels {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return CatsViewModel(
DiContainer.catFactService,
DiContainer.catImageService
) as T
}
}
}

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

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

catsView.findViewById<android.widget.Button>(R.id.button).setOnClickListener {
catsViewModel.loadCatData()
}

catsPresenter = CatsPresenter(diContainer.service)
view.presenter = catsPresenter
catsPresenter.attachView(view)
catsPresenter.onInitComplete()
observeViewModel()
catsViewModel.loadCatData()
}

override fun onStop() {
if (isFinishing) {
catsPresenter.detachView()
private fun observeViewModel() {
lifecycleScope.launch {
catsViewModel.catsState.collect { result ->
when (result) {
is Result.Loading -> {
catsView.showLoading()
}
is Result.Success -> {
catsView.populate(result.data)
}
is Result.Error -> {
catsView.showError(result.message)
}
}
}
}
super.onStop()
}
}
Loading