Retrofit 실습 예제 (2) Youtube API 가져오기

2024. 7. 31. 21:54[개발]/Kotlin 활용 앱 개발

# Retrofit 활용 순서 복기

1) 라이브러리 추가

dependencies {
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    // Gson Converter
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    // OKHttp for 통신 로그
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")

 

2) API 인터페이스 정의: 서비스의 각 HTTP 엔드포인트에 대해 메서드를 정의하는 인터페이스 생성

interface YoutubeAPI {
    // 예시) https://teamsparta.notion.site/Retrofit-41cdf5459d2c4fe2a14264121aad2dd8
    // baseURL: https://teamsparta.notion.site/
    @GET("videos")
    suspend fun getTrendingVideos(
        @Query("part") part: String = "snippet", // 필수 요청 매개변수
        @Query("chart") chart: String = "mostPopular", // 이하 선택 요청 매개변수
        @Query("maxResults") maxResults: Int = 100,
        @Query("regionCode") regionCode: String = "US",
        @Query("key") apiKey: String = API_KEY
    ): VideoResponse
}

 

참고: 유튜브 API 발급 사이트(https://developers.google.com/youtube/v3/docs/videos/list?hl=ko)

GET + 엔드포인트

 

3) Retrofit 인스턴스 생성

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create

// 싱글톤 패턴 위해 object로 선언
object RetrofitClient {
	// baseURL 넣는 부분 외에는 항상 동일
    private const val BASE_URL = "https://www.googleapis.com/youtube/v3/"

    private val retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            // 가져올 때 로그 찍어줌 -> 오류가 났을 때 쉽게 확인 가능
            .client(
                OkHttpClient.Builder()
                    .addInterceptor(HttpLoggingInterceptor().apply {
                        level = HttpLoggingInterceptor.Level.BODY
                    }).build()
            )
            // JSON 파일을 GSON으로 컨버팅
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    val youtubeAPI: YoutubeAPI by lazy { retrofit.create() }
}

 

4) API 호출

	override suspend fun getTrendingVideos(region: String): VideoResponse {
        return RetrofitClient.youtubeAPI.getTrendingVideos(regionCode = region)
    }
}

 

5) 인터넷 권한 추가

<uses-permission android:name="android.permission.INTERNET" />

android:usesCleartextTraffic="true"

 

※ 'Kotlin data class file from Json' 플러그인 활용해서 쉽게 data class 만들기

1) File Settings → Plugins에 json 검색

2) JSON To Kotlin Class 플러그인 install

3) data class를 생성하고 싶은 폴더에서 New → Kotlin data class file from Json

4) 원본 데이터 JSON 파일 복붙, class name 지정 

5)  Advanced에서 Property: val, nullable로 설정, Annotation: Gson으로 설정 후 Generate

6) 폴더로 가면 data class 파일이 알아서 생성되어 있음!

 

* 서버에서 모든 데이터를 다 가져와서 파일이 너무 많으면 보기가 힘들다! 내가 쓸 것들만 따로 모아두는 걸 추천

fun List<Item>.toVideoItem(): List<ListItem.VideoItem> {
	return this.map {
    	ListItem.VideoItem(
        	channelTitle = it.snippet?.channelTitle ?: "", 
            title = it.snippet?.title ?: "", 
            thumbnail = it.snippet?.thumbnails.high?.uri ?: "",
            description = it.snippet?.description ?: ""
        )
    }
}

 

# 실제 예제

// 1. API 인터페이스 정의
import retrofit2.http.GET
import retrofit2.http.Query

private const val API_MAX_RESULT = 20
private const val API_REGION = "US"
private const val API_KEY = BuildConfig.YOUTUBE_API_KEY

interface YoutubeAPI {
    @GET("videos")
    suspend fun getTrendingVideos(
        @Query("part") part: String = "snippet",
        @Query("chart") chart: String = "mostPopular",
        @Query("maxResults") maxResults: Int = API_MAX_RESULT,
        @Query("regionCode") regionCode: String = API_REGION,
        @Query("key") apiKey: String = API_KEY
    ): VideoResponse
}
// 2. Retrofit 인터페이스 생성
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create

object RetrofitClient {
    private const val BASE_URL = "https://www.googleapis.com/youtube/v3/"

    private val retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            // 디버깅용
            .client(
                OkHttpClient.Builder()
                    .addInterceptor(HttpLoggingInterceptor().apply {
                        level = HttpLoggingInterceptor.Level.BODY
                    }).build()
            )
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    val youtubeAPI: YoutubeAPI by lazy { retrofit.create() }
}

 

// 3. 필요한 데이터 클래스만 모아서 데이터 클래스 생성
import com.google.gson.annotations.SerializedName

data class VideoResponse(
    @SerializedName("etag")
    val etag: String?,
    @SerializedName("items")
    val items: List<Item>?,
    @SerializedName("kind")
    val kind: String?,
    @SerializedName("nextPageToken")
    val nextPageToken: String?,
    @SerializedName("pageInfo")
    val pageInfo: PageInfo?
)
data class Item(
    @SerializedName("etag")
    val etag: String?,
    @SerializedName("id")
    val id: String?,
    @SerializedName("kind")
    val kind: String?,
    @SerializedName("snippet")
    val snippet: Snippet?
)

data class Snippet(
    @SerializedName("categoryId")
    val categoryId: String?,
    @SerializedName("channelId")
    val channelId: String?,
    @SerializedName("channelTitle")
    val channelTitle: String?,
    @SerializedName("description")
    val description: String?,
    @SerializedName("liveBroadcastContent")
    val liveBroadcastContent: String?,
    @SerializedName("localized")
    val localized: Localized?,
    @SerializedName("publishedAt")
    val publishedAt: String?,
    @SerializedName("tags")
    val tags: List<String?>?,
    @SerializedName("thumbnails")
    val thumbnails: Thumbnails?,
    @SerializedName("title")
    val title: String?
)

 

// 4. 생성한 파일들을 연결하는 작업 in repository

package com.example.standardcloneui.data.repository

import ...

class YoutubeRepositoryImpl : VideoRepository {

	override suspend fun getTrendingVideos(region: String): VideoResponse {
    	return RetrofitClient.youtubeAPI.getTrendingVideos(regionCode = region)
    }	
}

 

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException

private const val TAG = "HomeViewModel"

class HomeViewModel(private val repository: VideoRepository = YoutubeRepositoryImpl()) :
    ViewModel() {
    private val _trendingVideos = MutableLiveData<List<ListItem.VideoItem>?>()
    val trendingVideos: LiveData<List<ListItem.VideoItem>?> = _trendingVideos

    fun fetchTrendingVideos(region: String = "US") {
        viewModelScope.launch {
            runCatching {
                val videos = repository.getTrendingVideos(region).items?.toVideoItem()
                _trendingVideos.value = videos
            }.onFailure {
                Log.e(TAG, "fetchTrendingVideos() failed! : ${it.message}")
                handleException(it)
            }
        }
    }

    private fun handleException(e: Throwable) {
        when (e) {
            is HttpException -> {
                val errorJsonString = e.response()?.errorBody()?.string()
                Log.e(TAG, "HTTP error: $errorJsonString")
            }

            is IOException -> Log.e(TAG, "Network error: $e")
            else -> Log.e(TAG, "Unexpected error: $e")
        }
    }
}

 

Viewmodel 안 쓰고 그냥 Fragment에서 쓰고 싶으면 ↓

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels

class HomeFragment : Fragment() {

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

    private val viewModel by viewModels<HomeViewModel>()

    private val videoAdapter by lazy {
        VideoListAdapter { video ->
            if (video !is ListItem.VideoItem) return@VideoListAdapter
            (activity as? MainActivity)?.showDetailFragment(video)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
        initViewModel()
    }

    private fun initView() = with(binding) {
        recyclerView.adapter = videoAdapter
        chipKorea.setOnClickListener {
            viewModel.fetchTrendingVideos("KR")
        }

        chipUs.setOnClickListener {
            viewModel.fetchTrendingVideos("US")
        }
    }

    private fun initViewModel() = with(viewModel) {
        trendingVideos.observe(viewLifecycleOwner) {
            videoAdapter.submitList(it)
        }
        fetchTrendingVideos("US")
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}