diff --git a/app/build.gradle b/app/build.gradle index a414e0e8..0aa0c687 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,8 @@ android { } dependencies { + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7" + implementation "androidx.fragment:fragment-ktx:1.8.5" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.12.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' diff --git a/app/src/main/java/otus/homework/coroutines/CatLoader.kt b/app/src/main/java/otus/homework/coroutines/CatLoader.kt new file mode 100644 index 00000000..f16d67dd --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatLoader.kt @@ -0,0 +1,7 @@ +package otus.homework.coroutines + +interface CatLoader { + + fun onInitComplete(): Unit + +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsFactService.kt b/app/src/main/java/otus/homework/coroutines/CatsFactService.kt new file mode 100644 index 00000000..53c88c85 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatsFactService.kt @@ -0,0 +1,11 @@ +package otus.homework.coroutines + +import otus.homework.coroutines.dto.Fact +import retrofit2.http.GET + +interface CatsFactService { + + @GET("fact") + suspend fun getCatFact() : Fact + +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsImageService.kt b/app/src/main/java/otus/homework/coroutines/CatsImageService.kt new file mode 100644 index 00000000..2a3558c7 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/CatsImageService.kt @@ -0,0 +1,11 @@ +package otus.homework.coroutines + +import otus.homework.coroutines.dto.CatImage +import retrofit2.http.GET + +interface CatsImageService { + + @GET("v1/images/search") + suspend fun getCatImage() : List + +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt index e4b05120..0a70d590 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsPresenter.kt @@ -1,28 +1,35 @@ package otus.homework.coroutines -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import otus.homework.coroutines.dto.CatImage + class CatsPresenter( - private val catsService: CatsService -) { + private val catsFactService: CatsFactService, + private val catsImagesService: CatsImageService +): CatLoader { private var _catsView: ICatsView? = null - fun onInitComplete() { - catsService.getCatFact().enqueue(object : Callback { + override fun onInitComplete() { + + PresenterScope.launch { + try { + + val catFact = async { catsFactService.getCatFact() }.await() - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful && response.body() != null) { - _catsView?.populate(response.body()!!) + val catImage: List = async { catsImagesService.getCatImage() }.await() + + _catsView?.populate(catFact, catImage.first()) + } catch (ex: Exception) { + if (ex is java.net.SocketTimeoutException) { + _catsView?.onError("Unable to get response from server") } + CrashMonitor.trackWarning(ex.message!!) } + } - override fun onFailure(call: Call, t: Throwable) { - CrashMonitor.trackWarning() - } - }) } fun attachView(catsView: ICatsView) { diff --git a/app/src/main/java/otus/homework/coroutines/CatsView.kt b/app/src/main/java/otus/homework/coroutines/CatsView.kt index be04b2a8..c3f4370b 100644 --- a/app/src/main/java/otus/homework/coroutines/CatsView.kt +++ b/app/src/main/java/otus/homework/coroutines/CatsView.kt @@ -3,8 +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 +import otus.homework.coroutines.dto.CatImage +import otus.homework.coroutines.dto.Fact +import otus.homework.coroutines.vm.CatsPresenterViewModel class CatsView @JvmOverloads constructor( context: Context, @@ -12,7 +18,7 @@ class CatsView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr), ICatsView { - var presenter :CatsPresenter? = null + var presenter :CatLoader? = null override fun onFinishInflate() { super.onFinishInflate() @@ -21,12 +27,21 @@ class CatsView @JvmOverloads constructor( } } - override fun populate(fact: Fact) { + override fun populate(fact: Fact, image: CatImage) { findViewById(R.id.fact_textView).text = fact.fact + Picasso.get().load(image.url).into(findViewById(R.id.cat_imageView)); } + + override fun onError(text: String) { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + } interface ICatsView { - fun populate(fact: Fact) + fun populate(fact: Fact, image: CatImage) + + fun onError(text: String) + } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt b/app/src/main/java/otus/homework/coroutines/CrashMonitor.kt index 32e6b018..562c4878 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 { + private final val TAG = javaClass.simpleName /** * Pretend this is Crashlytics/AppCenter */ - fun trackWarning() { + fun trackWarning(text: String) { + Log.e(TAG, text) } } \ 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..a15d17f1 100644 --- a/app/src/main/java/otus/homework/coroutines/DiContainer.kt +++ b/app/src/main/java/otus/homework/coroutines/DiContainer.kt @@ -5,12 +5,22 @@ import retrofit2.converter.gson.GsonConverterFactory class DiContainer { - private val retrofit by lazy { + private val catsFactRetrofit by lazy { Retrofit.Builder() .baseUrl("https://catfact.ninja/") .addConverterFactory(GsonConverterFactory.create()) .build() } - val service by lazy { retrofit.create(CatsService::class.java) } + val catsFactService by lazy { catsFactRetrofit.create(CatsFactService::class.java) } + + private val catsImagesRetrofit by lazy { + Retrofit.Builder() + .baseUrl("https://api.thecatapi.com/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + val catsImagesService by lazy { catsImagesRetrofit.create(CatsImageService::class.java) } + } \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/MainActivity.kt b/app/src/main/java/otus/homework/coroutines/MainActivity.kt index a9dafb3b..a5ad6075 100644 --- a/app/src/main/java/otus/homework/coroutines/MainActivity.kt +++ b/app/src/main/java/otus/homework/coroutines/MainActivity.kt @@ -2,6 +2,12 @@ package otus.homework.coroutines import androidx.appcompat.app.AppCompatActivity import android.os.Bundle +import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels +import androidx.lifecycle.Observer +import kotlinx.coroutines.CoroutineExceptionHandler +import otus.homework.coroutines.dto.Result +import otus.homework.coroutines.vm.CatsPresenterViewModel class MainActivity : AppCompatActivity() { @@ -9,16 +15,38 @@ class MainActivity : AppCompatActivity() { private val diContainer = DiContainer() + private val catsPresenterViewModel: CatsPresenterViewModel by viewModels { + CatsPresenterViewModel.provideFactory(diContainer.catsFactService, diContainer.catsImagesService, this) + } + 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() +// catsPresenter = CatsPresenter(diContainer.catsFactService, diContainer.catsImagesService) +// view.presenter = catsPresenter +// catsPresenter.attachView(view) +// catsPresenter.onInitComplete() + + catsPresenterViewModel.onInitComplete() + view.presenter = catsPresenterViewModel + + catsPresenterViewModel.catImageFact.observe(this, Observer { result -> + when (result) { + is Result.Success -> { + val image = result.data.first + val fact = result.data.second + view.populate(fact, image) + } + + is Result.Error -> + view.onError(result.message!!) + } + + }) + } override fun 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..24d70409 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/PresenterScope.kt @@ -0,0 +1,22 @@ +package otus.homework.coroutines + +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlin.coroutines.CoroutineContext + +object PresenterScope : CoroutineScope, AutoCloseable { + + private val job = Job() + + override val coroutineContext: CoroutineContext + get() { + return Dispatchers.Main + job + CoroutineName("CatsCoroutine") + } + + override fun close() { + job.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/dto/CatImage.kt b/app/src/main/java/otus/homework/coroutines/dto/CatImage.kt new file mode 100644 index 00000000..bafb4352 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/dto/CatImage.kt @@ -0,0 +1,14 @@ +package otus.homework.coroutines.dto + +import com.google.gson.annotations.SerializedName + +data class CatImage( + @field:SerializedName("id") + val id: String, + @field:SerializedName("url") + val url: String, + @field:SerializedName("width") + val width: Int, + @field:SerializedName("height") + val height: Int +) \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/dto/Fact.kt b/app/src/main/java/otus/homework/coroutines/dto/Fact.kt new file mode 100644 index 00000000..3b0194d6 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/dto/Fact.kt @@ -0,0 +1,10 @@ +package otus.homework.coroutines.dto + +import com.google.gson.annotations.SerializedName + +data class Fact( + @field:SerializedName("fact") + val fact: String, + @field:SerializedName("length") + val length: Int +) \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/dto/Result.kt b/app/src/main/java/otus/homework/coroutines/dto/Result.kt new file mode 100644 index 00000000..0c69a35c --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/dto/Result.kt @@ -0,0 +1,6 @@ +package otus.homework.coroutines.dto + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val message: String?, val throwable: Throwable? = null) : Result() +} \ No newline at end of file diff --git a/app/src/main/java/otus/homework/coroutines/vm/CatsPresenterViewModel.kt b/app/src/main/java/otus/homework/coroutines/vm/CatsPresenterViewModel.kt new file mode 100644 index 00000000..803e7e93 --- /dev/null +++ b/app/src/main/java/otus/homework/coroutines/vm/CatsPresenterViewModel.kt @@ -0,0 +1,72 @@ +package otus.homework.coroutines.vm + +import android.os.Bundle +import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.savedstate.SavedStateRegistryOwner +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import otus.homework.coroutines.CatLoader +import otus.homework.coroutines.dto.CatImage +import otus.homework.coroutines.CatsFactService +import otus.homework.coroutines.CatsImageService +import otus.homework.coroutines.CrashMonitor +import otus.homework.coroutines.dto.Fact +import otus.homework.coroutines.dto.Result +import kotlin.Pair + +class CatsPresenterViewModel( + private val catsFactService: CatsFactService, + private val catsImagesService: CatsImageService +) : ViewModel(), CatLoader { + + companion object { + fun provideFactory(catsFactService: CatsFactService, + catsImagesService: CatsImageService, + owner: SavedStateRegistryOwner, + defaultArgs: Bundle? = null,): AbstractSavedStateViewModelFactory =object : AbstractSavedStateViewModelFactory(owner, defaultArgs) { + @Suppress("UNCHECKED_CAST") + override fun create( + key: String, + modelClass: Class, + handle: SavedStateHandle + ): T { + return CatsPresenterViewModel(catsFactService,catsImagesService) as T + } + } + } + + private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> + CrashMonitor.trackWarning(throwable.message.toString()) + _catImageFact.postValue(Result.Error("", throwable)) + } + + + private val _catImageFact = MutableLiveData>>() + val catImageFact: LiveData>> get() = _catImageFact + + override fun onInitComplete() { + viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) { + try { + val imageResponse = async{ catsImagesService.getCatImage()} + val factResponse = async{ catsFactService.getCatFact()} + + val awaitAll = awaitAll(imageResponse, factResponse) + + val image = awaitAll[0] as List + val fact = awaitAll[1] as Fact + + _catImageFact.postValue(Result.Success(Pair(image[0], fact))) + + } catch (e: Exception) { + e.printStackTrace() + _catImageFact.postValue(Result.Error("", e)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9508066d..2bbef62e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,6 +7,15 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + + app:layout_constraintTop_toBottomOf="@id/cat_imageView" />