Skip to content

ДЗ Coroutines #3

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 4 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ dependencies {
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.squareup.picasso:picasso:2.71828'

//coroutine retrofit
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$retrofit_coroutines_adapter"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Несколько последних версий ретрофита поддерживают саспенд функции, так что этот адаптер уже не нужен

implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
}
56 changes: 44 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,48 @@
package otus.homework.coroutines

import retrofit2.Call
import retrofit2.Callback
import android.util.Log
import kotlinx.coroutines.*
import retrofit2.Response
import kotlin.coroutines.CoroutineContext

class CatsPresenter(
private val catsService: CatsService
private val catsServiceFact: CatsService,
private val catsServiceImage: CatsService
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем тебе два инстанса одного и того же стейтлесс класса?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

для того чтобы вызывать https://aws.random.cat/ который на другом ретрофит клиенте

private val retrofitImage by lazy {
Retrofit.Builder()
.baseUrl("https://aws.random.cat/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

) {

private var _catsView: ICatsView? = null
private val presenterScope =
PresenterScope(Job(), Dispatchers.Main, CoroutineName("CatsCoroutine"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Здесь можно Job не указывать. Имеет смысл если ты хочешь передать уже созданную Job в дочерний контекст или если хочешь передать SupervisorJob


fun onInitComplete() {
catsService.getCatFact().enqueue(object : Callback<Fact> {
presenterScope.launch {
try {
val factResponse = getCatFactResponse()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

У тебя запрос на Dispatchers.Main. Нужно переключить контекст и поменять диспатчер на Dispatchers.IO

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавил withContext(Dispatcher.IO) в CatsViewModel, в предудущем комите тольков CatsPresenter добавлял. Это решает проблему

val imageResponse = getCatImageResponse()
if (factResponse != null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А разве у тебя getCatFactResponse может вернуть нулл?

&& imageResponse != null ) {
val factImage = FactImage(factResponse, imageResponse)
_catsView?.populate(factImage)
} else
CrashMonitor.trackWarning()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это лучше перенести в последний catch


override fun onResponse(call: Call<Fact>, response: Response<Fact>) {
if (response.isSuccessful && response.body() != null) {
_catsView?.populate(response.body()!!)
}
} catch (e: java.net.SocketTimeoutException) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно не полный импорт указывать

_catsView?.showToast("Не удалось получить ответ от сервером")
} catch (e: Exception) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно сделать один catch блок и через when блок проверять исключение по типу

_catsView?.showToast(e.message.toString())
e.printStackTrace()
}

override fun onFailure(call: Call<Fact>, t: Throwable) {
CrashMonitor.trackWarning()
}
})
}
}

private suspend fun getCatFactResponse(): Fact {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Эти функции лишние. Лучше их удалить.

return catsServiceFact.getCatFact()
}


private suspend fun getCatImageResponse(): Image {
return catsServiceImage.getCatImage()
}

fun attachView(catsView: ICatsView) {
Expand All @@ -31,5 +51,17 @@ class CatsPresenter(

fun detachView() {
_catsView = null
presenterScope.cancel()
}
}

class PresenterScope(
private val job: Job,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Смотри тут получается что у тебя не PresenterScope, а просто какой то динамический скоуп холдер. Лучше убери аргументы из конструктора и с ними возможность менять этот скоуп

private val dispatchers: CoroutineDispatcher,
private val coroutineName: CoroutineName
) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + dispatchers + coroutineName


}
7 changes: 6 additions & 1 deletion app/src/main/java/otus/homework/coroutines/CatsService.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package otus.homework.coroutines

import kotlinx.coroutines.Deferred
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.GET

interface CatsService {

@GET("random?animal_type=cat")
fun getCatFact() : Call<Fact>
suspend fun getCatFact() : Fact

@GET("meow")
suspend fun getCatImage() : Image
}
26 changes: 22 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,30 +3,48 @@ 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 android.widget.Toast.LENGTH_LONG
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
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
var presenter: CatsPresenter? = null
private var refreshLayout: SwipeRefreshLayout? = null


override fun onFinishInflate() {
super.onFinishInflate()
findViewById<Button>(R.id.button).setOnClickListener {
presenter?.onInitComplete()
}
refreshLayout = findViewById<SwipeRefreshLayout>(R.id.swipe)
refreshLayout?.setOnRefreshListener {
presenter?.onInitComplete()
}
}

override fun populate(factImage: FactImage) {
refreshLayout?.isRefreshing = false
findViewById<TextView>(R.id.fact_textView).text = factImage.fact.text
Picasso.get().load(factImage.image.file).into(findViewById<ImageView>(R.id.iv_image))
}

override fun populate(fact: Fact) {
findViewById<TextView>(R.id.fact_textView).text = fact.text
override fun showToast(message: String) {
Toast.makeText(context, message, LENGTH_LONG).show()
}
}

interface ICatsView {

fun populate(fact: Fact)
fun populate(factImage: FactImage)
fun showToast(message: String)
}
58 changes: 58 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,58 @@
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.SupervisorJob
import kotlinx.coroutines.launch

class CatsViewModel(
private val catsServiceFact: CatsService,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Опять лишний класс-зависимость

private val catsServiceImage: CatsService
): ViewModel() {
private var _catsResponse = MutableLiveData<Result<FactImage>>()
val catsResponse: LiveData<Result<FactImage>>
get() = _catsResponse

fun getCatFactImage(){
viewModelScope.launch(SupervisorJob() + CoroutineExceptionHandler{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

У viewModelScope уже есть SupervisorJob в качестве элемента контекста

coroutineContext, throwable -> CrashMonitor.trackWarning()
}) {
try {
val factResponse = getCatFactResponse()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

У тебя не параллельные запросы. Сначала вызывается первый и корутина саспендится. После того как он заканчивается и она резьюмится начинает работать второй. Используй async чтобы распараллелить

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вопрос: изначально я сделал без async чтобы они выполнялись полседовательно
чтобы два результата использовать при создании объекта
FactImage(factResponse, imageResponse)
теперь при использовании async как это будет работать?
допустим получаем результат factResponse, imageResponse - неодновременно
FactImage(factResponse, imageResponse) будет создан из того что пришло первым,
а второй параметр "уйдет" пустым
или
родительская корутину (которая запускает launch)
"дождется" всех и только потом создаст FactImage(factResponse, imageResponse) ?

val imageResponse = getCatImageResponse()
if (factResponse != null && imageResponse != null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Они не могут быть null

val factImage = FactImage(factResponse, imageResponse)
_catsResponse.value = Result.Success(factImage)
} else{
_catsResponse.value = Result.Error("Что то пошло не так", null)
}

} catch (e: java.net.SocketTimeoutException) {
_catsResponse.value = Result.Error("Не удалось получить ответ от сервером", e)
} catch (e: Exception) {
_catsResponse.value = Result.Error(e.message.toString(), e)
e.printStackTrace()
}
}
}

private suspend fun getCatFactResponse(): Fact {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ненужные функции

return catsServiceFact.getCatFact()
}


private suspend fun getCatImageResponse(): Image {
return catsServiceImage.getCatImage()
}
}

sealed class Result<out T>{
data class Success<out R>(val value: R): Result<R>()
data class Error(
val message: String,
val throwable: Throwable?
): Result<Nothing>()
}
15 changes: 12 additions & 3 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,23 @@ package otus.homework.coroutines
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class DiContainer {
class DiContainer() {

private val retrofit by lazy {
private val retrofitFact by lazy {
Retrofit.Builder()
.baseUrl("https://cat-fact.herokuapp.com/facts/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}

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

val serviceFact by lazy { retrofitFact.create(CatsService::class.java) }
val serviceImage by lazy { retrofitImage.create(CatsService::class.java) }

}
8 changes: 8 additions & 0 deletions app/src/main/java/otus/homework/coroutines/FactImage.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 FactImage(
val fact: Fact,
val image: Image
)
10 changes: 10 additions & 0 deletions app/src/main/java/otus/homework/coroutines/FactImageResponse.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 FactImageResponse(
val fact: Fact? = null,
val image: Image? = null,
var isSuccessful: Boolean? = true,
var errorMessage: String? = null
)
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

import com.google.gson.annotations.SerializedName

data class Image(
@field:SerializedName("file")
val file: String
)
47 changes: 43 additions & 4 deletions app/src/main/java/otus/homework/coroutines/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,62 @@ package otus.homework.coroutines

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.squareup.picasso.Picasso

class MainActivity : AppCompatActivity() {

lateinit var catsPresenter: CatsPresenter

private val diContainer = DiContainer()
private lateinit var viewModel: CatsViewModel

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()
/*
### Реализовать решение ViewModel
*/
viewModel = CatsViewModel(diContainer.serviceFact, diContainer.serviceImage)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут неправильное использование ViewModel. Основная идея ViewModel в том что она умеет переживать пересоздание активити, достигается это за счет хранилища ViewModel. Поэтому есть определенные требования к тому как их создавать. Создавать их правильно не через конструктор, а через фактори метод ViewModelProviders.of Так ты привязываешь ViewModel к LifecycleOwner. И последний момент - чтобы создать ViewModel с не дефолтным конструктором ты должен реализовать Factory.

public class CatsViewModel extends ViewModelProvider.NewInstanceFactory {

   private String name;

   public CatsViewModel(String name) {
       super();
       this.name = name;
   }

   @NonNull
   @Override
   public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
       if (modelClass == CatsViewModel.class) {
           return (T) new CatsViewModel(name);
       }
       return null;
   }
}


viewModel.getCatFactImage()

val refreshLayout = findViewById<SwipeRefreshLayout>(R.id.swipe)
refreshLayout?.setOnRefreshListener {
viewModel.getCatFactImage()
}

viewModel.catsResponse.observe(this, Observer { factImage ->
refreshLayout.isRefreshing = false
when (factImage) {
is Result.Success -> {
findViewById<TextView>(R.id.fact_textView).text = factImage.value.fact?.text
Picasso.get().load(factImage.value.image?.file)
.into(findViewById<ImageView>(R.id.iv_image))
}
is Result.Error -> {
Toast.makeText(this, factImage.message, Toast.LENGTH_LONG).show()
}
}
})

//++++++++++++++++++++++++++++
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Комментарии лучше убрать)

/*
### Перейти с коллбеков на саспенд функции и корутины
### Добавить к запросу фактов запрос рандомных картинок с [https://aws.random.cat/meow](https://aws.random.cat/meow)
*/
// catsPresenter = CatsPresenter(diContainer.serviceFact, diContainer.serviceImage)
// view.presenter = catsPresenter
// catsPresenter.attachView(view)
// catsPresenter.onInitComplete()

}

override fun onStop() {
Expand Down
Loading