diff --git a/app/build.gradle b/app/build.gradle index 679dbba4..b8055665 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,13 @@ plugins { } android { - compileSdkVersion 30 + compileSdkVersion 33 buildToolsVersion "30.0.3" defaultConfig { applicationId "otus.homework.coroutines" minSdkVersion 23 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 1 versionName "1.0" @@ -33,6 +33,8 @@ android { } dependencies { + def coroutines_version = '1.6.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.2' implementation 'com.squareup.retrofit2:retrofit:2.9.0' @@ -42,4 +44,7 @@ dependencies { implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'com.squareup.picasso:picasso:2.71828' -} \ No newline at end of file + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" +} diff --git a/app/src/main/java/otus/homework/coroutines/CatDetails.kt b/app/src/main/java/otus/homework/coroutines/CatDetails.kt new file mode 100644 index 00000000..fe70902b --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatDetails.kt @@ -0,0 +1,14 @@ +package otus.homework.coroutines + +import com.google.gson.annotations.SerializedName + +data class CatDetails( + @field:SerializedName("id") + val id: String, + @field:SerializedName("created_at") + val createdAt: String, + @field:SerializedName("tags") + val tags: List = emptyList(), + @field:SerializedName("url") + val url: String +) diff --git a/app/src/main/java/otus/homework/coroutines/CatFactWithImage.kt b/app/src/main/java/otus/homework/coroutines/CatFactWithImage.kt new file mode 100644 index 00000000..a3c276b0 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatFactWithImage.kt @@ -0,0 +1,7 @@ +package otus.homework.coroutines + + +data class CatFactWithImage( + val fact: String, + val imageUrl: String +) diff --git a/app/src/main/java/otus/homework/coroutines/CatImageService.kt b/app/src/main/java/otus/homework/coroutines/CatImageService.kt new file mode 100644 index 00000000..dba42eaa --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatImageService.kt @@ -0,0 +1,10 @@ +package otus.homework.coroutines + +import retrofit2.http.GET +import retrofit2.http.Query + +interface CatImageService { + + @GET("cat") + suspend fun getCatImage(@Query("json") json: Boolean = true): CatDetails +} diff --git a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt index e4b05120..ee239c55 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt @@ -1,28 +1,32 @@ package otus.homework.coroutines -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.net.SocketTimeoutException class CatsPresenter( - private val catsService: CatsService + private val catsService: CatsService, + private val catImageService: CatImageService ) { - + private val tag = this.javaClass.simpleName private var _catsView: ICatsView? = null + private val scope = PresenterScope() fun onInitComplete() { - catsService.getCatFact().enqueue(object : Callback { - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - _catsView?.populate(response.body()!!) - } - } - - override fun onFailure(call: Call, t: Throwable) { - CrashMonitor.trackWarning() + scope.launch { + try { + val fact = async { catsService.getCatFact() } + val image = async { catImageService.getCatImage() } + val catInfo = CatFactWithImage(fact.await().fact, image.await().url) + _catsView?.populate(catInfo) + } catch (e: SocketTimeoutException) { + _catsView?.showError(R.string.error_failed_to_get_response_from_server) + } catch (e: Exception) { + _catsView?.showError(e.message) + CrashMonitor.trackWarning(tag, e) } - }) + } } fun attachView(catsView: ICatsView) { @@ -30,6 +34,7 @@ class CatsPresenter( } fun detachView() { + scope.cancel() _catsView = null } -} \ No newline at end of file +} diff --git a/app/src/main/java/otus/homework/coroutines/CatsService.kt b/app/src/main/java/otus/homework/coroutines/CatsService.kt index 479b2cfb..4f4ff549 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsService.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsService.kt @@ -1,10 +1,9 @@ package otus.homework.coroutines -import retrofit2.Call import retrofit2.http.GET interface CatsService { @GET("fact") - fun getCatFact() : Call -} \ No newline at end of file + suspend fun getCatFact(): Fact +} diff --git a/app/src/main/java/otus/homework/coroutines/CatsView.kt b/app/src/main/java/otus/homework/coroutines/CatsView.kt index 30ac2531..60a88627 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsView.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsView.kt @@ -3,8 +3,12 @@ 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.annotation.StringRes import androidx.constraintlayout.widget.ConstraintLayout +import com.squareup.picasso.Picasso class CatsView @JvmOverloads constructor( context: Context, @@ -12,7 +16,7 @@ class CatsView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView { - var presenter :CatsPresenter? = null + var presenter: CatsPresenter? = null override fun onFinishInflate() { super.onFinishInflate() @@ -21,12 +25,28 @@ class CatsView @JvmOverloads constructor( } } - override fun populate(fact: Fact) { - findViewById(R.id.fact_textView).text = fact.text + override fun populate(fact: CatFactWithImage) { + findViewById(R.id.fact_textView).text = fact.fact + val url = "https://cataas.com${fact.imageUrl}" + Picasso.get().load(url).into( + findViewById(R.id.cat_imageView) + ) + } + + override fun showError(error: String?) { + val errorText = error ?: context.getString(R.string.error_unknown) + Toast.makeText(context, errorText, Toast.LENGTH_SHORT).show() + } + + override fun showError(@StringRes error: Int) { + Toast.makeText(context, error, Toast.LENGTH_SHORT).show() } } interface ICatsView { - fun populate(fact: Fact) -} \ No newline at end of file + fun populate(fact: CatFactWithImage) + + fun showError(error: String?) + fun showError(@StringRes error: Int) +} diff --git a/app/src/main/java/otus/homework/coroutines/CatsViewModel.kt b/app/src/main/java/otus/homework/coroutines/CatsViewModel.kt new file mode 100644 index 00000000..701185e3 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatsViewModel.kt @@ -0,0 +1,39 @@ +package otus.homework.coroutines + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +class CatsViewModel( + private val catsService: CatsService, + private val catImageService: CatImageService +) : ViewModel() { + + private var _stateLiveData = MutableLiveData>() + val stateLiveData: LiveData> get() = _stateLiveData + + init { + loadData() + } + + fun loadData() { + viewModelScope.launch(CoroutineExceptionHandler { _, throwable -> + _stateLiveData.value = Result.Error(Error(throwable.message)) + CrashMonitor.trackWarning("", throwable) + }) { + try { + val fact = async { catsService.getCatFact() } + val image = async { catImageService.getCatImage() } + val catInfo = CatFactWithImage(fact.await().fact, image.await().url) + _stateLiveData.value = Result.Success(catInfo) + } catch (e: Exception) { + _stateLiveData.value = Result.Error(Error("Не удалось получить ответ от сервера")) + CrashMonitor.trackWarning("", e) + } + } + } +} diff --git a/app/src/main/java/otus/homework/coroutines/CatsViewModelFactory.kt b/app/src/main/java/otus/homework/coroutines/CatsViewModelFactory.kt new file mode 100644 index 00000000..2d8671b7 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatsViewModelFactory.kt @@ -0,0 +1,15 @@ +package otus.homework.coroutines + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class CatsViewModelFactory : ViewModelProvider.Factory { + + private val diContainer = DiContainer() + private val catsService: CatsService = diContainer.catService + private val catImageService: CatImageService = diContainer.catImageService + + override fun create(modelClass: Class): T { + return CatsViewModel(catsService, catImageService) as T + } +} diff --git a/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt b/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt index 32e6b018..b9dd484d 100644 --- a/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt +++ b/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt @@ -1,10 +1,13 @@ package otus.homework.coroutines +import android.util.Log + object CrashMonitor { /** * Pretend this is Crashlytics/AppCenter */ - fun trackWarning() { + fun trackWarning(tag: String, t: Throwable) { + Log.e(tag, t.message ?: "Unknown error") } -} \ No newline at end of file +} diff --git a/app/src/main/java/otus/homework/coroutines/DiContainer.kt b/app/src/main/java/otus/homework/coroutines/DiContainer.kt index 23ddc3b2..c068e29a 100644 --- a/app/src/main/java/otus/homework/coroutines/DiContainer.kt +++ b/app/src/main/java/otus/homework/coroutines/DiContainer.kt @@ -5,12 +5,21 @@ import retrofit2.converter.gson.GsonConverterFactory class DiContainer { - private val retrofit by lazy { + private val catFactRetrofit by lazy { Retrofit.Builder() .baseUrl("https://catfact.ninja/") .addConverterFactory(GsonConverterFactory.create()) .build() } - val service by lazy { retrofit.create(CatsService::class.java) } -} \ No newline at end of file + val catService by lazy { catFactRetrofit.create(CatsService::class.java) } + + private val catImageRetrofit by lazy { + Retrofit.Builder() + .baseUrl("https://cataas.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + val catImageService by lazy { catImageRetrofit.create(CatImageService::class.java) } +} diff --git a/app/src/main/java/otus/homework/coroutines/Fact.kt b/app/src/main/java/otus/homework/coroutines/Fact.kt index 15c6c7ae..e30e2fc1 100644 --- a/app/src/main/java/otus/homework/coroutines/Fact.kt +++ b/app/src/main/java/otus/homework/coroutines/Fact.kt @@ -3,22 +3,8 @@ package otus.homework.coroutines import com.google.gson.annotations.SerializedName data class Fact( - @field:SerializedName("createdAt") - val createdAt: String, - @field:SerializedName("deleted") - val deleted: Boolean, - @field:SerializedName("_id") - val id: String, - @field:SerializedName("text") - val text: String, - @field:SerializedName("source") - val source: String, - @field:SerializedName("used") - val used: Boolean, - @field:SerializedName("type") - val type: String, - @field:SerializedName("user") - val user: String, - @field:SerializedName("updatedAt") - val updatedAt: String -) \ No newline at end of file + @field:SerializedName("fact") + val fact: String, + @field:SerializedName("length") + val length: Int +) diff --git a/app/src/main/java/otus/homework/coroutines/MainActivity.kt b/app/src/main/java/otus/homework/coroutines/MainActivity.kt index a9dafb3b..bcf77fed 100644 --- a/app/src/main/java/otus/homework/coroutines/MainActivity.kt +++ b/app/src/main/java/otus/homework/coroutines/MainActivity.kt @@ -2,12 +2,14 @@ package otus.homework.coroutines import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.lifecycle.ViewModelProvider class MainActivity : AppCompatActivity() { - lateinit var catsPresenter: CatsPresenter +// lateinit var catsPresenter: CatsPresenter +// private val diContainer = DiContainer() - private val diContainer = DiContainer() + lateinit var vm: CatsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -15,16 +17,31 @@ class MainActivity : AppCompatActivity() { 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() - } - - override fun onStop() { - if (isFinishing) { - catsPresenter.detachView() + vm = ViewModelProvider(this, CatsViewModelFactory()).get(CatsViewModel::class.java) + vm.stateLiveData.observe(this) { result -> + when (result) { + is Result.Success -> { + view.populate(result.value) + } + + is Result.Error -> { + view.showError(result.throwable.toString()) + } + } } - super.onStop() + + view.setOnClickListener { vm.loadData() } + +// catsPresenter = CatsPresenter(diContainer.catService, diContainer.catImageService) +// view.presenter = catsPresenter +// catsPresenter.attachView(view) +// catsPresenter.onInitComplete() } -} \ No newline at end of file + +// override fun onStop() { +// if (isFinishing) { +// catsPresenter.detachView() +// } +// super.onStop() +// } +} diff --git a/app/src/main/java/otus/homework/coroutines/PresenterScope.kt b/app/src/main/java/otus/homework/coroutines/PresenterScope.kt new file mode 100644 index 00000000..57e99e0b --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/PresenterScope.kt @@ -0,0 +1,13 @@ +package otus.homework.coroutines + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + +class PresenterScope : CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + CoroutineName("CatsCoroutine") + Job() +} diff --git a/app/src/main/java/otus/homework/coroutines/Result.kt b/app/src/main/java/otus/homework/coroutines/Result.kt new file mode 100644 index 00000000..f6acf726 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/Result.kt @@ -0,0 +1,6 @@ +package otus.homework.coroutines + +sealed interface Result { + class Success(val value: T) : Result + class Error(val throwable: Throwable) : Result +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9508066d..d4d84570 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,20 +3,29 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:padding="16dp" android:layout_height="match_parent" + android:padding="16dp" tools:context=".MainActivity"> + + + app:layout_constraintTop_toBottomOf="@+id/cat_imageView" />