[TIL] ๐ŸŒผ24/05/01๐ŸŒผ#Sealed Class์™€ Flow๋กœ Retrofit ์‘๋‹ต ์ฒ˜๋ฆฌ

0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
93/104
post-thumbnail
post-custom-banner

[TIL] ๐ŸŒผ24/05/01๐ŸŒผ#Sealed Class์™€ Flow๋กœ Retrofit ์‘๋‹ต ์ฒ˜๋ฆฌ

Sealed Class์™€ Flow๋กœ Retrofit ์‘๋‹ต ์ฒ˜๋ฆฌ

  • ์นด์นด์˜ค ์ด๋ฏธ์ง€/๋™์˜์ƒ ๊ฒ€์ƒ‰ API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒ€์ƒ‰ ์•ฑ์—์„œ
    API ์‘๋‹ต์„ Sealed Class์™€ Flow๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ–ˆ๋‹ค!

๐Ÿ“Œ์ฐธ๊ณ ์ž๋ฃŒ

sealed class Result<out T : Any> {
    data class Success<out T : Any>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}
fun handleApiResponse(response: ApiResponse) {
    when (response) {
        is ApiResponse.Success -> {
            // handle success
        }
        is ApiResponse.Error -> {
            // handle error
        }
        ApiResponse.Loading -> {
            // handle loading
        }
    }
}
  • API ์‘๋‹ต ์ฒ˜๋ฆฌ์— ์‚ฌ์šฉํ•  Sealed Class ์ •์˜ํ•˜๊ธฐ (ApiResponse.kt)
    • ์ œ๋„ค๋ฆญ ํƒ€์ž… ์‚ฌ์šฉ -> ์—ฌ๋Ÿฌ API ์‘๋‹ต ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
sealed class ApiResponse<out T>{
    object Loading: ApiResponse<Nothing>()
    data class Success<T>(val data: T): ApiResponse<T>()
    sealed class Fail: ApiResponse<Nothing>() {
        data class Error(val code: Int, val message: String?) : Fail()
        data class Exception(val e: Throwable) : Fail()
    }
}
  • ์นด์นด์˜ค ์ด๋ฏธ์ง€/๋™์˜์ƒ ๊ฒ€์ƒ‰ API Retrofit ํ†ต์‹  ์‘๋‹ต์œผ๋กœ ๋ฐ›๋Š” DTO ํด๋ž˜์Šค (KakaoSearchDTO.kt)
data class KakaoSearchDTO<T:Document>(
    val meta: Meta,
    val documents: List<T>?
)

data class Meta(
    @SerializedName("total_count")
    val totalCount: Int,
    @SerializedName("pageable_count")
    val pageableCount: Int,
    @SerializedName("is_end")
    val isEnd: Boolean
)

sealed interface Document {
    data class ImageDocument(
        val collection: String,
        @SerializedName("thumbnail_url")
        val thumbnailUrl: String,
        @SerializedName("image_url")
        val imageUrl: String,
        val width: Int,
        val height: Int,
        @SerializedName("display_sitename")
        val displaySitename: String,
        @SerializedName("doc_url")
        val docUrl: String,
        val datetime: String
    ):Document

    data class VideoDocument(
        val title: String,
        val url: String,
        val datetime: String,
        @SerializedName("play_time")
        val playTime: Int,
        val thumbnail: String,
        val author: String
    ):Document
}
  • Repository์—์„œ API ์‘๋‹ต(KakaoSearchDTO)์„ Sealed Class(ApiResponse)๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ emit ํ•˜๊ธฐ (ItemRepositoryImpl.kt)
class ItemRepositoryImpl @Inject constructor(
    private val kakaoSearchSource: KakaoSearchApi
) : ItemRepository {
    override suspend fun getImages(query: String): Flow<ApiResponse<KakaoSearchDTO<Document.ImageDocument>>> =
        handleKakaoSearchDTO {
            kakaoSearchSource.getImageDTO(query)
        }

    override suspend fun getVideos(query: String): Flow<ApiResponse<KakaoSearchDTO<Document.VideoDocument>>> =
        handleKakaoSearchDTO {
            kakaoSearchSource.getVideoDTO(query)
        }

    private fun <T:Document> handleKakaoSearchDTO(
        execute: suspend () -> KakaoSearchDTO<T>
    ): Flow<ApiResponse<KakaoSearchDTO<T>>> = flow {
        emit(ApiResponse.Loading)
        try {
            emit(ApiResponse.Success(execute()))
        } catch (e: HttpException) {
            emit(ApiResponse.Fail.Error(e.code(), e.message()))
        } catch (e: Exception) {
            emit(ApiResponse.Fail.Exception(e))
        }
    }
}
  • API ์‘๋‹ต ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ViewModel์—์„œ emit๋œ ๊ฐ’ collectํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ (SearchViewModel.kt)
    • ์ด๋ฏธ์ง€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์™€ ๋™์˜์ƒ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ combine์œผ๋กœ ํ•ฉ์ณ์„œ ์‚ฌ์šฉ
	private val _searchItems = MutableStateFlow<List<Item>>(emptyList())
    
    private suspend fun fetchSearchResult() {
        val query = _keyword.value ?: ""

        val imageResponseFlow = itemRepository.getImages(query)
        val videoResponseFlow = itemRepository.getVideos(query)

        imageResponseFlow.combine(videoResponseFlow) {i, v ->
            val itemList = mutableListOf<Item>()
            if(i is ApiResponse.Success){
                itemList.addAll(i.data.documents?.map { it.convert() } ?: emptyList())
            }
            if(v is ApiResponse.Success){
                itemList.addAll(v.data.documents?.map { it.convert() } ?: emptyList())
            }
            itemList.toList()
        }.collect{
            _searchItems.value = it
        }
    }

    private fun Document.ImageDocument.convert() =
        Item(
            itemType = ItemType.IMAGE_TYPE,
            imageUrl = imageUrl,
            source = displaySitename,
            time = datetime.formatDate()
        )

    private fun Document.VideoDocument.convert() =
        Item(
            itemType = ItemType.VIDEO_TYPE,
            imageUrl = thumbnail,
            source = author,
            time = datetime.formatDate()
        )
post-custom-banner

0๊ฐœ์˜ ๋Œ“๊ธ€